Linux上のプログラムからODBCでDBMSに接続する

# はじめに
ここではLinux上のユーザープログラムが、同じくLinux上で稼働するDBMSにODBC経由で接続するための手順を示す。前提とするプラットフォームおよびDBMSは以下のとおり。

- ユーザープログラムが稼働するOS : Alma Linux 9.5 DVD ISO
- ユーザープログラムを構成する技術 : gccで作成したネイティブアプリ
- DBMSが稼働するOS : Alma Linux 9.5 DVD ISO
- DBMS : PostgreSQL 17.2
- ODBC Driver : psqlODBC 17.00
- ODBC Driver Manager : unixODBC 2.3.9

# ユーザープログラムとDBMSの関係
ユーザープログラムは、何らかのConnectorを介してDBMSに接続する。DBMSごとに多種のConnectorが提供されており、ODBCはそのひとつであり、代表的なものとなる。
ODBC経由でDBMSに接続するには、DBMSの開発元から提供されているODBC Driverを予めクライアント側(ユーザープログラム側の環境)に組み込んでおく必要がある。また、ユーザープログラムは、ODBC Driver Managerを介してODBC Driverに接続する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/385813/cf7bf56f-a25d-0d40-bc2f-873b98697ef9.png)
Windowsの場合、ODBC Driver Managerは、OSに組み込まれているため、その存在を特別意識しなくても良かったが、Linuxの場合、ODBC Driverと同様に、ODBC Driver Managerもクライアント側に組み込んでおく必要がある。
Linux用のODBC Driver Managerとしては、下記のものがよく知られている。

__unixODBC__
- 概要: 最も一般的で広く使用されているオープンソースのODBC Driver Manager。
- 特徴: 多くのODBC Driverやアプリケーションでサポートされている。
- 利用シーン: 多くのLinuxディストリビューションにおける標準のODBC Driver Manager。
- ライセンス: GNU General Public License (GPL) および GNU Lesser General Public License (LGPL) 

__iODBC (Independent Open DataBase Connectivity)__
- 概要: unixODBCと同様にオープンソースのODBC Driver Manager。最初にリリースされたODBC Driver Managerひとつで、クロスプラットフォーム対応を目指してる。
- 特徴: macOSや他のUnix系システムでも広く使用されている。
- 利用シーン: 特定の商用アプリケーションやiODBCに特化した環境で使われることがある。
- ライセンス: BSD License 

# 環境構築
### DBMS、ODBC Driver/Driver Managerのインストール
DBMSのインストールについては、数多くの情報源があり、その設定方法も様々であるため、ここでは詳細を事細かに示すことはせず、PostgreSQLに絞り、最低限の記載にとどめる。また、ODBC Driver Managerについては、unixODBCを使用する。

PostgreSQLの最低限の動作に必要な、下記のパッケージをダウンロードする。
_postgresql17-libs-17.2-1PGDG.rhel9.x86_64.rpm_
_postgresql17-17.2-1PGDG.rhel9.x86_64.rpm_
_postgresql17-server-17.2-1PGDG.rhel9.x86_64.rpm_
_postgresql17-odbc-17.00.0004-1PGDG.rhel9.x86_64.rpm_

上記は、PostgreSQLの公式サイトからダウンロード可能である。
https://download.postgresql.org/pub/repos/yum/17/redhat/rhel-9-x86_64/

また、unixODBCは、RPMのダウンロードサイトからダウンロード可能である。
_unixODBC-devel-2.3.9-4.el9.x86_64.rpm_
_unixODBC-devel-2.3.9-4-devel.el9.x86_64.rpm_

ダウンロード後、下記のコマンドでインストールする。インストールする順番を間違えるとエラーとなる。
```
$ sudo rpm -ivh postgresql17-libs-17.2-1PGDG.rhel9.x86_64.rpm
$ sudo rpm -ivh postgresql17-17.2-1PGDG.rhel9.x86_64.rpm
$ sudo rpm -ivh postgresql17-server-17.2-1PGDG.rhel9.x86_64.rpm
$ sudo rpm -ivh unixODBC-devel-2.3.9-4.el9.x86_64.rpm
$ sudo rpm -ivh unixODBC-devel-2.3.9-4-devel.el9.x86_64.rpm
$ sudo rpm -ivh postgresql17-odbc-17.00.0004-1PGDG.rhel9.x86_64.rpm
```

### PostgreSQL、ODBC Driver Managerの設定
PostgreSQLおよびunixODBCのインストール後、PostgreSQLおよびunixODBCの初期設定を行う。postgresユーザーでinitdbを実行する。コマンド実行時にpostgresユーザーのパスワードの設定を求められる。ここではパスワード=postgresとする。
```
$sudu su - postgres
$ /usr/pgsql-17/bin/initdb -E UTF8 --locale=C -A scram-sha-256 -W
```
postgresユーザーのまま、/var/lib/pgsql/17/data/pg_hba.confが下記のようになっていることを確認する。
```
local all all scram-sha-256
host all all 127.0.0.1/32 scram-sha-256
host all all ::1/128 scram-sha-256
```
同じく/var/lib/pgsql/17/data/postgresql.confを下記のように編集する。
```
listen_address='localhost'
Port=5432
```
このあと、postgresユーザーをexitで抜けて下記を実行する。
```
$ sudo systemctl start postgresql-17.service
$ sudo systemctl enable postgresql-17.service
```
postgresユーザーとして、psqlでデータベース、テーブル、レコードを追加する。
```
$ psql -U postgres
# create database testdb;
# \l
... testdbを含むデータベース一覧が表示される ...
# \c testdb
# create table abc (id integer, name varchar(255));
# insert into abc values (0, 'hello');
```
上記で、\l はデータベース一覧を表示するコマンドで、\cはデータベースに接続するコマンドである。
postgresユーザーでのpsqlの実行後、psqlを抜けて、/etc/odbc.iniおよび/etc/odbcinst.iniを変更する。
```
# odbcinst.ini:
[PostgreSQL]
Driver=/usr/pgsql-17/lib/psqlodbcw.so
Setup=/usr/lib64/unixODBC/libodbcpsqlS.so
Driver64=/usr/pgsql-17/lib/psqlodbcw.so
Setup64=/usr/lib64/unixODBC/libodbcpsqlS.so
FileUsage=1
```
```
# odbc.ini:
[PostgreSQL_DS]
Driver=PostgreSQL
Database=testdb
Servername=localhost
Port=5432
UserName=postgres
Password=postgres
```
下記isqlでtestdbに接続し、abcテーブルの情報を参照できることを確認する。
```
$ isql PostgreSQL_DS
SQL> select * from abc;
```
# ユーザープログラム
下記のプログラムは、DBMSに接続し、切断するだけのサンプルコードである。下記でコンパイルおよび実行することができる。
```
$ gcc testdb.cpp -lodbc -lodbcinst -o testdb
$ ./testdb
```

```
#include <cstdlib>
#include <cstdio>
#include <sql.h>
#include <sqlext.h>

SQLHENV  henv;
SQLHDBC  hdbc;
SQLHSTMT hstmt;

// Return 0: Success, -1:Error
int open_database(SQLCHAR* connect_str, SQLCHAR statemsg[10], SQLCHAR msg[1024])
{
	SQLINTEGER native; // This will not be refered from anywhere
	SQLSMALLINT actual_msg_len; // This will not be refered from anywhere

	// Alloc environment handle
	if (SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv) == SQL_ERROR) {
		if (henv != SQL_NULL_HENV) {
			SQLGetDiagRec(SQL_HANDLE_ENV, henv, 1, statemsg, &native, msg, 1024, &actual_msg_len);
		}
		return -1;
	}
	SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, reinterpret_cast<SQLPOINTER>(SQL_OV_ODBC3), 0);

	// Alloc DB connection handle
	if (SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc) == SQL_ERROR) {
		SQLGetDiagRec(SQL_HANDLE_ENV, henv, 1, statemsg, &native, msg, 1024, &actual_msg_len);
		return -1;
	}

	// SQLDriverConnect
	SQLCHAR conn_out[255]; // This will not be refered from anywhere
	SQLSMALLINT conn_out_len; // This will not be refered from anywhere
	SQLRETURN Ret = SQLDriverConnect(hdbc, NULL, connect_str, SQL_NTS, conn_out, 255, &conn_out_len, SQL_DRIVER_COMPLETE);
	if (Ret == SQL_ERROR || Ret == SQL_SUCCESS_WITH_INFO) {
		SQLGetDiagRec(SQL_HANDLE_DBC, hdbc, 1, statemsg, &native, msg, 1024, &actual_msg_len);
		return -1;
	}

	// Alloc statement handle 
	if (SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt) == SQL_ERROR) {
		SQLGetDiagRec(SQL_HANDLE_DBC, hdbc, 1, statemsg, &native, msg, 1024, &actual_msg_len);
		return -1;
	}

	return 0;
}

// Return 0: Success, -1:Error
int close_database(SQLCHAR statemsg[10], SQLCHAR msg[1024])
{
	SQLINTEGER native; // This will not be refered from anywhere
	SQLSMALLINT actual_msg_len; // This will not be refered from anywhere

	// Free statement handle
	if (SQLFreeHandle(SQL_HANDLE_STMT, hstmt) == SQL_ERROR) {
		SQLGetDiagRec(SQL_HANDLE_DBC, hdbc, 1, statemsg, &native, msg, 1024, &actual_msg_len);
		return -1;
	}

	// SQLDisconnect
	SQLRETURN Ret = SQLDisconnect(hdbc);
	if (Ret == SQL_ERROR || Ret == SQL_SUCCESS_WITH_INFO) {
		SQLGetDiagRec(SQL_HANDLE_DBC, hdbc, 1, statemsg, &native, msg, 1024, &actual_msg_len);
		return -1;
	}

	// Free DB connection handle
	if (SQLFreeHandle(SQL_HANDLE_DBC, hdbc) == SQL_ERROR) {
		SQLGetDiagRec(SQL_HANDLE_ENV, henv, 1, statemsg, &native, msg, 1024, &actual_msg_len);
		return -1;
	}

	// Free environment handle
	if (SQLFreeHandle(SQL_HANDLE_ENV, henv) == SQL_ERROR) {
		SQLGetDiagRec(SQL_HANDLE_ENV, henv, 1, statemsg, &native, msg, 1024, &actual_msg_len);
		return -1;
	}

	return 0;
}

int main(int argc, char* argv[])
{
	SQLCHAR statemsg[10];
	SQLCHAR msg[1024];
	SQLCHAR connect_str[64] = "DSN=PostgreSQL_DS;";
	if (open_database(connect_str, statemsg, msg) == -1) {
		printf("statemsg=%s, msg=%s\n", (char*)statemsg, (char*)msg);
		return -1;
	}
	if (close_database(statemsg, msg) == -1) {
		printf("statemsg=%s, msg=%s\n", (char*)statemsg, (char*)msg);
		return -1;
	}
	printf("success!\n");
	return 0;
}
```
接続文字列の指定では、前述isqlの実行と同様にodbc.iniで設定したデータソース名'PostgreSQL_DS'を指定しているが、odbc.iniおよびodbcinst.iniに記載した個別設定をconnet_strに指定することもできる。
```
SQLCHAR connect_str[128] = "Driver=/usr/pgsql-17/lib/psqlodbcw.so;Server=127.0.0.1;Database=testdb;UID=postgres;PWD=postgres;Port=5432;";
```