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が実行されて、下のように同様の結果を画面に表示することができた。

FinderやXcodeから/usr/localを開けるようにする

Macでは、/usrなどのUNIXシステム領域は、Finderから通常見れないようになっている。
これはこれで安全なので良いことだが、例えば、/usr/localに独自にプログラムをインストールしていたり、そこに置いたライブラリを呼び出すようなアプリを作っていたりすると、FinderやXcodeから/usr/localを開きたくなる場合がある。
その場合、以下のような手順で/usr/localをFinderのサイドバーに登録すると、FinderやXcodeから/usr/localを開けるようにできる。

1.まず、Finderのメニューから「移動」→「フォルダに移動」を選択。

2.移動先フォルダに「/usr」を入力

3.「/usr」フォルダの内容が表示されるので、「local」をサイドバーの「場所」配下にドラッグする。
注意:誤って、「local」以外のフォルダを、変な場所にドラッグしてしまうと、トラブルを起こす可能性もあるので、十分注意して作業してください。

4.Finderのサイドバーに「local」フォルダが出来る。これで完了。後は通常のサイドバーと同様に開くことが出来る。


なお、サイドバー設定をするとXcodeから/usr/local配下のファイルが開けるようになる。例えば、Xcodeで/usr/local配下のライブラリファイルをプロジェクトに加えたいときは、以下のように「追加」→「既存ファイル」を選択すると、

出てくるファイル選択画面の左側の「local」をクリックすれば/usr/localを開くことが出来る。

Macの起動時にPostgreSQLが自動起動されるように設定する

正確にはMacの起動時ではなく、ログインしたときに起動、ログアウトしたときに終了される設定。

起動、終了スクリプト作成

自分の環境では、postgresは/usr/local/pgsqlにインストールし、管理ユーザーはデフォルト通りpostgresであるため、以下の内容のスクリプトを作成。

起動スクリプト例:/usr/local/bin/LoginHook

#!/bin/sh
sudo -u postgres /usr/local/pgsql/bin/pg_ctl -D /usr/local/pgsql/data -l /Users/postgres/`date +%Y%m%d`.log start

終了スクリプト例:/usr/local/bin/LogoutHook

#!/bin/sh
sudo -u postgres /usr/local/pgsql/bin/pg_ctl -D /usr/local/pgsql/data stop

スクリプト自動実行設定

ここ(アップルのサイト)が参考になる。
以下のコマンドで、作成したスクリプトがログイン、ログアウト時に実行されるように設定する。

$ sudo defaults write com.apple.loginwindow LoginHook /usr/local/bin/LoginHook
$ sudo defaults write com.apple.loginwindow LogoutHook /usr/local/bin/LogoutHook

以下のコマンドで設定確認できる。

$ sudo defaults read com.apple.loginwindow LoginHook
/usr/local/bin/LoginHook
$ sudo defaults read com.apple.loginwindow LogoutHook
/usr/local/bin/LogoutHook

(2013年1月14日追記)削除は以下のコマンドでできる。

sudo defaults delete com.apple.loginwindow LogoutHook
sudo defaults delete com.apple.loginwindow LoginHook

MacOSXのRubyでメールをPOPダウンロードしてPostgreSQLデータベースに保存する

あるメールアドレスで受信したメールを、POPでダウンロードしてパソコン内のPostgreSQLデータベースに保存する仕組みがほしかったので、Rubyを使って実装してみた。

PostgreSQLデータベースに保存するために、ruby-pgというライブラリを、また、タイムスタンプとかヘッダーとかを取り出すために、TMailというライブラリを、それぞれインストールした。

動作環境

動作確認した環境: MacOSX 10.6.8 + Ruby 1.8.7Macに最初から入っていた) + PostgreSQL 8.4.10
メールアカウントはGmailを使用。

PostgreSQLは/usr/local/pgsqlにソースからビルドしてインストール済み(ただconfigureとmakeしただけ。標準的な構成のはず)。
また、以下のような定義のテーブルmailboxを、文字コードUTF8のデータベースdb_mailbox内に用意した。

CREATE TABLE mailbox
(
id serial NOT NULL,
dt timestamp with time zone,
subject character varying,
body text,
CONSTRAINT mailbox_pkey PRIMARY KEY (id )
)
WITH (
OIDS=FALSE
);
ALTER TABLE mailbox
OWNER TO postgres;

ruby-pgのインストール

gemというコマンドを使用して導入する。コマンドラインから以下を実行:

$ sudo gem install pg -- --with-pg-config=/usr/local/pgsql/bin/pg_config

Password:
Building native extensions. This could take a while...
ERROR: Error installing pg:
ERROR: Failed to build gem native extension.

/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby extconf.rb --with-pg-config=/usr/local/pgsql/bin/pg_config
Using config values from /usr/local/pgsql/bin/pg_config
checking for libpq-fe.h... yes
checking for libpq/libpq-fs.h... yes
checking for pg_config_manual.h... yes
checking for PQconnectdb() in -lpq... no
checking for PQconnectdb() in -llibpq... no
checking for PQconnectdb() in -lms/libpq... no
Can't find the PostgreSQL client library (libpq)
  :
  :
Gem files will remain installed in /Library/Ruby/Gems/1.8/gems/pg-0.13.2 for inspection.
Results logged to /Library/Ruby/Gems/1.8/gems/pg-0.13.2/ext/gem_make.out

失敗する。
いろいろ調べてみたところ、Rubyとは違うけどHDBC-postgresqlという環境で、PostgreSQLのアーキテクチャーがユニバーサルバイナリでないため導入に苦労したらしい、という情報があった。
自分はユニバーサルバイナリのことがあまりよくわかってないのだが、とにかくバイナリのアーキテクチャーの問題でPostgreSQLとの連携がうまくいかないのかもしれないと思い、ここを参考にfileコマンドでアーキテクチャーを確認してみた。

$ file /usr/local/pgsql/bin/postgres
/usr/local/pgsql/bin/postgres: Mach-O 64-bit executable x86_64

自分の場合はSnow Leopardの環境でビルドしていたので、PostgreSQLバイナリのアーキテクチャーはx86_64になっていた。
そのため、これに合わせてruby-pgも作る必要があったようだ。
以下のようにx86_64を指定して再度実行。

$ sudo env ARCHFLAGS='-arch x86_64' gem install pg -- --with-pg-config=/usr/local/pgsql/bin/pg_config
Password:
Building native extensions. This could take a while...
Successfully installed pg-0.13.2
1 gem installed
Installing ri documentation for pg-0.13.2...

Enclosing class/module 'rb_mPG' for class Connection not known

Enclosing class/module 'rb_mPG' for class Result not known
Installing RDoc documentation for pg-0.13.2...

Enclosing class/module 'rb_mPG' for class Connection not known

Enclosing class/module 'rb_mPG' for class Result not known
$

導入できた。

TMailのインストール

最初Rubyの標準機能で受信したメールを処理できないかと思ったが、試行錯誤した範囲ではできなかった。
ここの記事からTMailの存在を知り導入した。

ruby-pgと同様にgemで導入する。コマンドラインから以下を実行:

$ sudo gem install tmail
Password:
Building native extensions. This could take a while...
Successfully installed tmail-1.2.7.1
1 gem installed
Installing ri documentation for tmail-1.2.7.1...
Installing RDoc documentation for tmail-1.2.7.1...

Rubyスクリプト作成

実はPerlRubyのどっちを使うか迷ったのだが、NetBeansデバッグが出来ることからRubyを選んだ。
Windows版のNetBeans7ではRubyデバッグは出来なくなっているようだが、MacのNetBeans7では現時点で出来る)

以下のスクリプトを作成。こことかこことかこことかこことかここを参考にさせて頂きました。感謝。

require 'rubygems'
require 'net/pop'
require 'tmail'
require 'kconv'
require 'pg'

pop = Net::POP3.new('pop.gmail.com', 995)
pop.enable_ssl(OpenSSL::SSL::VERIFY_NONE)
pop.start('YourAccount, 'YourPassword')

db = PG.connect( :dbname => 'db_mailbox', :user => 'postgres')

if pop.mails.empty?
$stderr.puts 'no mail.'
else
pop.mails.each_with_index do |m, idx|
the_mail = TMail::Mail.parse(m.pop)
db.exec "INSERT INTO mailbox ( dt, subject, body ) VALUES ($1,$2,$3);", [the_mail.date, the_mail.subject.toutf8, the_mail.body.toutf8]
m.delete
end
$stderr.puts "#{pop.mails.size} mails popped."
end

pop.finish

POPダウンロードと文字コードの変換は、標準で入っていたライブラリで対応できた。

実行結果

以上の環境構築とスクリプトでメールをPostgreSQLデータベースに保存する仕組みが実現できた。

以下はテスト用のメールをGmailで見たところ。

スクリプトを実行すると、POPでダウンロードしてPostgreSQLのデータベースに保存できた。
以下は保存できたメールデータをpgadminで見たところ。

postgresの実行中のユーザ定義関数(ストアド)を途中で止める

今日うっかり重いユーザ定義関数を走らせてはまったので、重い処理のキャンセル方法を残す。
UNIXで言うとプロセスのkill、windowsで言うとタスクマネージャによるアプリの強制終了みたいなもの。


ここここが参考になる。
というより、それだけ見ればもう十分だが、ここでも一応やったことを残す。


まず、以下のSQLで現在動いているSQLを確認する。

SELECT pg_stat_get_backend_pid(s.backendid) AS procpid,
pg_stat_get_backend_activity(s.backendid) AS current_query
FROM (SELECT pg_stat_get_backend_idset() AS backendid) AS s;

以下実行例。

次に、確認したprocidを指定して以下のSQLを実行する。

SELECT pg_cancel_backend(確認したprocid);

以下実行例。


最初のSQLを再度実行すれば、停止したかどうか確認できる。
タイトルにはユーザ定義関数と書いたけど、もちろん普通のSQLも同様に中断できる。

ソニーはアップルの足下にも及ばない(PCメーカーとしては)

自分はジョブズの言行録とかそういう本はまだ一冊も買ってないし、買う気もない。ただ、ネットに出ているジョブズの記事にある発言を読む限りでは、なぜこの人が変人扱いされるのか私には全く理解できない。逆にジョブズを変人扱いする人の方を、妙なバイアスのかかっている人だなと感じてしまう。

「デザインとは外見の美しさだけでなく機能の美しさも意味する」というジョブズの発言は、特殊な主張でも何でも無く、実はデザインの世界ではごくあたりまえの主張である。私は大学にいた頃、デザイン論の講義を少しだけ聴いたことがあるが、一番入門の講義の一番最初で「デザインとは外見だけでなく機能のデザインも含めたものである」と講師が言っていた。そのくらい基本的な話である。誤解を恐れずに言えば、ジョブズの言う「デザイン」とは「仕様」の意味であると言ってもいいかもしれない。そういったデザインの原則を厳しく守ったところこそが、ジョブズの本質だったように思えて仕方が無い。

ジョブズエジソンと言うより、建築分野でのバウハウスのように、IT分野におけるデザインの地位を高めた人物として歴史に残るべきではないか。

いくつかのジョブズ追悼記事で、ジョブズソニーを意識していたという記事が出ている。しかし、少なくともPCメーカーとしては、ソニーはアップルの足下にも及ばないと私は思う。

なぜなら、ソニーVAIOのOSはWindowsであり、Macは独自OSであるからだ。アップルは一貫して独自OSの製品を作ろうとしてきたと思う。ジョブズのNeXTもそうだ。OSのオリジナリティは問題ではない。むしろOSの技術的な性格はオープンであった方がいい。重要なのは、製品として独自OSにしている、ということだ。独自OSということは、製品の仕様を全てコントロールできると共に、製品についての全ての責任を持つ、ということだ。

ソニーはじめ国内PCメーカーはオープン化の流れに負けて、独自OSのPCを製品にしていけなかった。だから、Windows Vistaの動作が遅くて評判が落ちたとき、VAIOを含む国内PCの評判も一緒に落ちたはずだ。確かにそれはMicrosoftの問題でソニーはじめPCメーカーの責任ではなかったかもしれない。だから明確にメーカーやPCの評判は落ちたかどうか、はっきりと確認はできなかったかもしれない。しかし、例えば、私が、Windows Vistaの遅さにうんざりし、VAIOを含む国内PCへの興味を失った事実に変わりは無い。

Macは独自OSで、ハードウェアも自分で作っていたから、そういった事実をはじめから発生させなかった。旧Macが不安定だったとしても、それは常にアップルの責任であった。そしてアップルは経営危機に落ち、そしてジョブズを迎えて再生した。

最初、「ソニージョブズの足下にも及ばない」というタイトルでジョブズをヨイショする雑文を書こうと思ったのだが、書いているうちにアップルをヨイショする文章になってしまったので「ソニーはアップルの足下にも及ばない」というタイトルにした。それからプレステとかについては、ソニーが製品についての全ての責任を持っていると思うから、「PCメーカーとしては」と但し書きをつけた。

ソニーは追悼の映画なんか作る暇があったら、VAIOから撤退して、プレステ一本でいくことを真剣に考えたらどうか。きっとジョブズもその方が喜ぶよ。

ジョブズ死去

本当に残念。
自分にとって彼の最大の功績は、やはりMacOSUNIXベースに作り替えてくれたこと。
NeXTの流れを引き継いで、外見も中身も全てにおいて美しく、高性能で安定し使いやすいPCを創造してくれた。
MacOSXがなかったら、自分は旧MacWindowsの不安定さに絶望し、Linuxのアプリの少なさに悩み、現在PCを触っていなかったかもしれない。

ジョブズは、コンピュータの性能が飛躍的に上昇していった時代だったからこそ成功し得た天才であったと思う。コンピュータという素材を、芸術的な方法で、個人が使いやすい形にまで料理し、提供してくれた。彼にまつわる様々な逸話は、そのことがどれほど困難な事業だったかを意味していると思う。しかし、彼は、それに挑戦し、成し遂げていった。
彼は、天才的な創造者であると同時に、時代に愛された人物でもあり、困難に挑戦した勇敢な男でもあった。