Macアプリ(Objective-C)からPostgreSQLのlibpqを直接使ってみる

Objective-C は C の完全上位互換なので、もしかしたら、単純に Objective-C から直接 libpq を叩けばそのまま動くのではないかと思って試してみたら、そのとおりだった。
実際にやったことを記録に残す。

環境

  • MacOS X 10.6.8
  • Xcode 3.2.6
  • PostgreSQL 8.4.10 (ソースからビルドして導入。単純にconfigure, make, make installしただけ。/usr/local/pgsqlに導入されている)

プロジェクト作成

Cocoa Application」を選択して、新規プロジェクトを作成。

インクルードファイルの検索パスを設定

インクルードファイル libpq-fe.h が /usr/local/pgsql/include にあるので、これに合わせてインクルードファイルの検索パスを設定する。
「プロジェクト」→「プロジェクト設定を編集」を選択して「プロジェクトの情報」を開く。

「プロジェクト・・・の情報」画面が出てくるので「ビルド」タブ(?)に切り替えて「ユーザヘッダ検索パス」に「/usr/local/pgsql/include」を設定する。

libpq.aをプロジェクトに追加

ライブラリ libpq.a が /usr/local/pgsql/lib にあるので、これをリンクできるようにプロジェクトに追加する。
libpq.aをプロジェクトフォルダ内にコピーしておくのも一つの方法と思うが、ここでは、PostgreSQLを導入した /usr/local/pgsql/lib 配下にある libpq.a を直接呼び出すことにする。
事前に、ここの方法であらかじめ /usr/local をFinderのサイドバーに登録しておけば、以下の手順で追加できる。今回はそうした。

Xcodeの画面の左側のプロジェクト名をControl+クリックしてコンテキストメニューを出し、「追加」→「既存のファイル」を選択する。

サイドバーから「local」を選択して、さらに「pgsql」「lib」と進んで、「libpq.a」を選択する。選択したら「追加」をクリックする。

追加方法を聞かれるので、そのまま「追加」をクリックする。

追加が成功すると以下のようにlibpq.aが表示される。

サンプルアプリ作成

PostgreSQLのソースフォルダ配下のsrc/test/examples内に、libpqを使用したサンプルプログラムがある。
このうち一番簡単なtestlibpq.cを流用して、サンプルアプリ(こちらで公開)を作成した。

流用元からの変更点は下記の通り:

  • libpq-fe.h は「#include」ではなく「#import」という記述を使ってインクルードする。
  • 問い合わせ結果を、fprintf, printf で標準出力に出力する代わりに、Cocoa API でウィンドウ上に表示するように変更。
  • SQLが異常終了した際に、画面は消さずに(アプリは終わらせずに)「PQfinish(conn);」のみコールして処理を戻すように変更。
  • めんどくさかったので接続文字列を実行時に指定できる機能は省略した。
//
//  This is sample Objective-C program of libpq, the PostgreSQL frontend library.
//  pgsql/src/test/examples/testlibpq.c is diverted to the "testlibpq" method in this program.
//

#import "libpq_testAppDelegate.h"
#import "libpq-fe.h"

@implementation libpq_testAppDelegate

@synthesize window;
@synthesize	outputField;

// --------------------------------------------------------------------------------
// 
// このメソッドは、pgsql/src/test/examples/testlibpq.cのmainを流用して作っています。
// mainから修正した行は「Modified for the Cocoa Application.」のコメントで挟んでいます。
//
// ----- Modified for the Cocoa Application. start -----
//int
//main(int argc, char **argv)
- (IBAction)testlibpq:(id)sender
// ----- Modified for the Cocoa Application. end -------
{
	const char *conninfo;
	PGconn	   *conn;
	PGresult   *res;
	int			nFields;
	int			i,
	j;
// ----- Modified for the Cocoa Application. start -----
	char		*wkmsg, *wkResultChar;
	NSString	*msg;
	NSMutableString	*resultStr;
	NSString	*wkResultStr;
// ----- Modified for the Cocoa Application. end -------
	
	/*
	 * If the user supplies a parameter on the command line, use it as the
	 * conninfo string; otherwise default to setting dbname=postgres and using
	 * environment variables or defaults for all other connection parameters.
	 */
// ----- Modified for the Cocoa Application. start -----
//	if (argc > 1)
//		conninfo = argv[1];
//	else
//	conninfo = "dbname = postgres";
		conninfo = "dbname = postgres user=postgres";
// ----- Modified for the Cocoa Application. end -------
	
	/* Make a connection to the database */
	conn = PQconnectdb(conninfo);
	
	/* Check to see that the backend connection was successfully made */
	if (PQstatus(conn) != CONNECTION_OK)
	{
// ----- Modified for the Cocoa Application. start -----
//		fprintf(stderr, "Connection to database failed: %s",
//				PQerrorMessage(conn));
		wkmsg = PQerrorMessage(conn);
		msg = [NSString stringWithFormat:@"Connection to database failed: %s", wkmsg];
		[outputField setStringValue:msg];
// ----- Modified for the Cocoa Application. end -------
// ----- Modified for the Cocoa Application. start -----
//		exit_nicely(conn);
		PQfinish(conn);
		return;
// ----- Modified for the Cocoa Application. end -------
	}
	
	/*
	 * Our test case here involves using a cursor, for which we must be inside
	 * a transaction block.  We could do the whole thing with a single
	 * PQexec() of "select * from pg_database", but that's too trivial to make
	 * a good example.
	 */
	
	/* Start a transaction block */
	res = PQexec(conn, "BEGIN");
	if (PQresultStatus(res) != PGRES_COMMAND_OK)
	{
// ----- Modified for the Cocoa Application. start -----
//		fprintf(stderr, "BEGIN command failed: %s", PQerrorMessage(conn));
		wkmsg = PQerrorMessage(conn);
		msg = [NSString stringWithFormat:@"BEGIN command failed: %s", wkmsg];
		[outputField setStringValue:msg];
// ----- Modified for the Cocoa Application. end -------
		PQclear(res);
// ----- Modified for the Cocoa Application. start -----
//		exit_nicely(conn);
		PQfinish(conn);
		return;
// ----- Modified for the Cocoa Application. end -------
	}
	
	/*
	 * Should PQclear PGresult whenever it is no longer needed to avoid memory
	 * leaks
	 */
	PQclear(res);
	
	/*
	 * Fetch rows from pg_database, the system catalog of databases
	 */
	res = PQexec(conn, "DECLARE myportal CURSOR FOR select * from pg_database");
	if (PQresultStatus(res) != PGRES_COMMAND_OK)
	{
// ----- Modified for the Cocoa Application. start -----
//		fprintf(stderr, "DECLARE CURSOR failed: %s", PQerrorMessage(conn));
		wkmsg = PQerrorMessage(conn);
		msg = [NSString stringWithFormat:@"DECLARE CURSOR failed: %s", wkmsg];
		[outputField setStringValue:msg];
// ----- Modified for the Cocoa Application. end -------
		PQclear(res);
// ----- Modified for the Cocoa Application. start -----
//		exit_nicely(conn);
		PQfinish(conn);
		return;
// ----- Modified for the Cocoa Application. end -------
	}
	PQclear(res);
	
	res = PQexec(conn, "FETCH ALL in myportal");
	if (PQresultStatus(res) != PGRES_TUPLES_OK)
	{
// ----- Modified for the Cocoa Application. start -----
//		fprintf(stderr, "FETCH ALL failed: %s", PQerrorMessage(conn));
		wkmsg = PQerrorMessage(conn);
		msg = [NSString stringWithFormat:@"FETCH ALL failed: %s", wkmsg];
		[outputField setStringValue:msg];
// ----- Modified for the Cocoa Application. end -------
		PQclear(res);
// ----- Modified for the Cocoa Application. start -----
//		exit_nicely(conn);
		PQfinish(conn);
		return;
// ----- Modified for the Cocoa Application. end -------
	}
	
	/* first, print out the attribute names */
// ----- Modified for the Cocoa Application. start -----
	resultStr = [NSMutableString string];
	wkResultStr = [[NSString alloc] init];
// ----- Modified for the Cocoa Application. end -------
	nFields = PQnfields(res);
	for (i = 0; i < nFields; i++)
// ----- Modified for the Cocoa Application. start -----
//		printf("%-15s", PQfname(res, i));
	{
		wkResultChar = PQfname(res, i);
		wkResultStr = [NSString stringWithFormat:@"%-15s", wkResultChar];
		[resultStr appendString:wkResultStr];
	}
// ----- Modified for the Cocoa Application. end -------
// ----- Modified for the Cocoa Application. start -----
//	printf("\n\n");
	[resultStr appendString:@"\n\n"];
// ----- Modified for the Cocoa Application. end -------
	
	/* next, print out the rows */
	for (i = 0; i < PQntuples(res); i++)
	{
		for (j = 0; j < nFields; j++)
// ----- Modified for the Cocoa Application. start -----
//			printf("%-15s", PQgetvalue(res, i, j));
		{
			wkResultChar = PQgetvalue(res, i, j);
			wkResultStr = [NSString stringWithFormat:@"%-15s", wkResultChar];
			[resultStr appendString:wkResultStr];
		}
// ----- Modified for the Cocoa Application. end -------
// ----- Modified for the Cocoa Application. start -----
//		printf("\n");
		[resultStr appendString:@"\n"];
// ----- Modified for the Cocoa Application. end -------
	}
	
	PQclear(res);
	
	/* close the portal ... we don't bother to check for errors ... */
	res = PQexec(conn, "CLOSE myportal");
	PQclear(res);
	
	/* end the transaction */
	res = PQexec(conn, "END");
	PQclear(res);
	
	/* close the connection to the database and cleanup */
	PQfinish(conn);
	
// ----- Modified for the Cocoa Application. start -----
	[outputField setStringValue:resultStr];
// ----- Modified for the Cocoa Application. end -------

// ----- Modified for the Cocoa Application. start -----
//	return 0;
// ----- Modified for the Cocoa Application. end -------
}
//
// ここまで流用
//
// --------------------------------------------------------------------------------

-(void)dealloc {
	[outputField release];
	[super dealloc];
}

@end

動作確認

以上のサンプルアプリで、testlibpq.cと同様のSQLが実行されて、下のように同様の結果を画面に表示することができた。