pg_xnodeを使ってみた
こんにちは。ぬこ@横浜(@nuko_yokoham)です。
このエントリはPostgreSQL Advent Calendar 2012 : ATNDの12/23用として登録しました(23日中に投稿できなかったのは私が怠けていたからです・・・すいません)。
pg_xnodeとは
pg_xnodeとは、簡単に言えばXML型を操作する拡張機能です。
PostgreSQLのコア機能としても(--with-libxml付きでconfigureを行えば)XML型、XML構築関数群、そしてXPath関数は使用できますが、pg_xnodeは幾つかの特徴的な機能を持っています。
あと、実装上面白いのは、このEXTENSIONがサードパーティーライブラリに依存していないというところでしょうか。XMLを処理するのであれば、libxml2、あるいはXerces-C、SAXONなどのXMLプロセッサライブラリを使うのが通例でしょうけど、どうやらこのEXTENSIONではXML処理、XPath処理を自前で実装しているようです。
pg_xnodeのビルド
pg_xnodeのサイトへアクセスしてダウンロードページへ移ります。ダウンロードページは現状、2つのリンクが置かれてますが、今回は"Source package of the current release can be downloaded"のほうのリンクを選択して pg_xnode-0.7.2.tar.gz をダウンロードします。
tar.gzファイルをPostgreSQLがインストールされているマシン上の任意の場所で展開し、make USE_PGXS=1でビルドします。
$ ls -l pg_xnode-0.7.2.tar.gz -rwxrw-rw-. 1 harada harada 151809 11月 8 02:43 2012 pg_xnode-0.7.2.tar.gz $ tar xfz pg_xnode-0.7.2.tar.gz $ cd pg_xnode-0.7.2 $ make USE_PGXS=1 ・・・(makeのログ) $ make USE_PGXS=1 install ・・・(make installのログ)
うちの環境だと数箇所gccから警告メッセージが出力されましたが、ビルド自体はできたのでとりあえず放置しておきます。;-)
PostgreSQLサーバが起動していれば、make installcheckでテストもできます。
$ make USE_PGXS=1 installcheck make -C src installcheck make[1]: ディレクトリ `/home/harada/src/pg_xnode-0.7.2/src' に入ります /home/harada/pgsql-9.2.2/lib/pgxs/src/makefiles/../../src/test/regress/pg_regress --inputdir=. --psqldir='/home/harada/pgsql-9.2.2/bin' --user=postgres --dbname=contrib_regression xnode update xnt (using postmaster on Unix socket, default port) ============== dropping database "contrib_regression" ============== DROP DATABASE ============== creating database "contrib_regression" ============== CREATE DATABASE ALTER DATABASE ============== running regression test queries ============== test xnode ... ok test update ... ok test xnt ... ok ===================== All 3 tests passed. =====================
これでpg_xnode EXTENSIONがデータベースにインストール可能になりました。
データベースにpg_xnodeをインストールします。
xdb=# CREATE EXTENSION xnode ; CREATE EXTENSION xdb=# \dx List of installed extensions Name | Version | Schema | Description ---------+---------+------------+---------------------------------- plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language xnode | 0.7.2 | xml | Implementation of XML using DOM. (2 rows) xdb=#
XMLデータの格納と参照
まず、pg_xnodeへデータを格納してみます。以下のようにテーブルを定義します。
xdb=# CREATE TABLE test (data1 xml.doc, data2 xml); CREATE TABLE xdb=# \d test Table "public.test" Column | Type | Modifiers --------+---------+----------- data1 | xml.doc | data2 | xml | xdb=#
data1の型 xml.doc がpg_xnodeが提供するXML文書格納用の型です。今回は比較のためにPostgreSQL組み込みのXML型のカラムも定義してみました。
まずINSERTで挿入して、SELECTで参照してみます。
xdb=# INSERT INTO test VALUES ('<a><b><c>c01</c><d>d01</d></b></a>','<a><b><c>c01</c><d>d01</d></b></a>'); INSERT 0 1 xdb=# INSERT INTO test VALUES ('<ns1:a xmlns:ns1="http://foo.bar"><ns1:b><ns1:c>c01</ns1:c><ns1:d>d01</ns1:d></ns1:b></ns1:a>', '<ns1:a xmlns:ns1="http://foo.bar"><ns1:b><ns1:c>c01</ns1:c><ns1:d>d01</ns1:d></ns1:b></ns1:a>'); INSERT 0 1 xdb=# INSERT INTO test VALUES ('<あ><い><う>う01</う><え>え01</え></い></あ>','<あ><い><う>う01</う><え>え01</え></い></あ>');; INSERT 0 1 xdb=# \x Expanded display is on. xdb=# SELECT * FROM test; -[ RECORD 1 ]---------------------------------------------------------------------------------------- data1 | <a><b><c>c01</c><d>d01</d></b></a> data2 | <a><b><c>c01</c><d>d01</d></b></a> -[ RECORD 2 ]---------------------------------------------------------------------------------------- data1 | <ns1:a xmlns:ns1="http://foo.bar"><ns1:b><ns1:c>c01</ns1:c><ns1:d>d01</ns1:d></ns1:b></ns1:a> data2 | <ns1:a xmlns:ns1="http://foo.bar"><ns1:b><ns1:c>c01</ns1:c><ns1:d>d01</ns1:d></ns1:b></ns1:a> -[ RECORD 3 ]---------------------------------------------------------------------------------------- data1 | <あ><い><う>う01</う><え>え01</え></い></あ> data2 | <あ><い><う>う01</う><え>え01</え></い></あ> xdb=#
XPathによる取り出し
次にXPathで中間ノードを取り出してみます。pg_xnodeでは xml.path() を使います。
xdb=# SELECT xml.path('/a/b', data1) FROM test; path ----------------------------- <b><c>c01</c><d>d01</d></b> (3 rows) xdb=#
このパスの場合、最初の行の文書しかヒットしないのですが、残りの2行の文書が空白なのかNULLなのかが分からないので\psetでNULL値を設定して再検証してみます。
xdb=# \pset null (null) Null display is "(null)". xdb=# SELECT xml.path('/a/b', data1) FROM test; path ----------------------------- <b><c>c01</c><d>d01</d></b> (null) (null) (3 rows) xdb=#
どうやら xml.path() ではヒットしなかった場合、NULLと評価されるようです。これは組み込みのxpath関数とは異なるのでちょっと注意が必要です。組み込みのxpath関数の場合は、ヒットしない場合以下のように空の配列として評価されます。
xdb=# SELECT xpath('/a/b', data2) FROM test; xpath -------------- {"<b> + <c>c01</c>+ <d>d01</d>+ </b>"} {} {} (3 rows) xdb=#
別のpath関数
pg_xnodeにはもうひとつ別形式のpath関数があります。
このpath関数は
を引数とし、ベースパス+ベースパスからの相対パスにヒットするノードを配列として返却します。
xdb=# SELECT xml.path('/a/b', '{"c", "d"}', data1) FROM test; path ------------------------- {<c>c01</c>,<d>d01</d>} (1 row) xdb=#
ここで注目すべきは結果が1行になっているところです。どうやらベースパスに合致しない文書はレコードとしても生成されない挙動になっているようです。ここも組み込みのxpathとは大きく異るところでしょう。
名前空間
最初に文書を登録したときには気づかなかったのですが、現状のpg_xnodeではまだ名前空間をきちんと扱えないようです。
PostgreSQL組み込みのxpath関数では、第3引数に名前空間接頭辞と名前空間URLの組みを指定できるので、以下のように任意の名前空間接頭辞と名前空間URLを関連づけられます。
xdb=# SELECT xpath('/ns1:a/ns1:b/ns1:c', '<ns1:a xmlns:ns1="http://foo" xmlns:ns2="http://bar"><ns1:b><ns1:c>c01</ns1:c><ns2:c>c02</ns2:c></ns1:b></ns1:a>', ARRAY[['ns1','http://foo'],['ns2','http://bar']]); xpath ---------------------- {<ns1:c>c01</ns1:c>} (1 row) xdb=# SELECT xpath('/foo:a/foo:b/foo:c', '<ns1:a xmlns:ns1="http://foo" xmlns:ns2="http://bar"><ns1:b><ns1:c>c01</ns1:c><ns2:c>c02</ns2:c></ns1:b></ns1:a>', ARRAY[['foo','http://foo'],['bar','http://bar']]); xpath ---------------------- {<ns1:c>c01</ns1:c>} (1 row) xdb=#
が、pg_xnodeの場合には名前空間の指定がそもそもできないので、上記のような指定はできません。
xdb=# SELECT xml.path('/ns1:a/ns1:b/ns1:c', '<ns1:a xmlns:ns1="http://foo" xmlns:ns2="http://bar"><ns1:b><ns1:c>c01</ns1:c><ns2:c>c02</ns2:c></ns1:b></ns1:a>'); path -------------------- <ns1:c>c01</ns1:c> (1 row) xdb=# SELECT xml.path('/foo:a/foo:b/foo:c', '<ns1:a xmlns:ns1="http://foo" xmlns:ns2="http://bar"><ns1:b><ns1:c>c01</ns1:c><ns2:c>c02</ns2:c></ns1:b></ns1:a>'); path -------- (null) (1 row) xdb=#
まあ、このあたりは今後改善されていくのではないかと思いますが・・・。
おわりに
今回はここまでですが、今後、文書内へのノード挿入・ノード削除、検索結果とテンプレートからXMLを生成する機能なども検証していきたいと思います。
おまけ:pg_xnodeのバイナリ形式
バイナリ形式はどうなっているのかな・・・?と思ってバイナリ入出力関数があるかどうか確認したが
CREATE TYPE doc ( internallength = variable, input = doc_in, output = doc_out, alignment = int, storage = extended );
残念ながらサポートされていない。なので、自分でバイナリ出力関数を追加してみました。
byteasendを真似て、src/xmlnode.cに以下のようなコードを追加し
PG_FUNCTION_INFO_V1(xmldoc_send); Datum xmldoc_send(PG_FUNCTION_ARGS) { bytea *vlena = PG_GETARG_BYTEA_P_COPY(0); PG_RETURN_BYTEA_P(vlena); }
SQLファイルに関数登録し、doc型にsend定義を追加。
CREATE FUNCTION doc_send(doc) RETURNS bytea as 'MODULE_PATHNAME', 'xmldoc_send' LANGUAGE C IMMUTABLE STRICT; CREATE TYPE doc ( internallength = variable, input = doc_in, output = doc_out, send = doc_send, alignment = int, storage = extended );
再度EXTENSIONを登録して実験。
xnodedb=# SELECT data, xml.doc_send(data) FROM test; -[ RECORD 1 ]---------------------------------------------------------------------------------------------------------------- data | <a><b><c>c01</c><d>d01</d></b></a> doc_send | \x070063303100020001000663000700643031000002000100076400000200020016086200020001000861000000000100080000002c000000 xnodedb=#
なんかバイナリ形式が出力されましたねw
"633031"や"643031"あたりがテキストノード"c01","d01"の部分だと思います。ちなみに長さは56バイトなので格納効率的にはそんなに良くはなさそうですが、このあたりのバイナリ解析も今後余裕があれば見ておきたいです。