2018年12月1日

Python版dblinkでデータベース連携をもっと「自由」に

本エントリは、 PostgreSQL Advent Calendar 2018 の Day1 のエントリです。

エントリを書くのは実に半年以上ぶりなのですが、今回は以前から試してみたかったdblinkネタをお届けします。

■なぜ今さら「dblink」?


PostgreSQLには、PostgreSQL、あるいは異種DBMSのデータベース連携を実現する手段として、dblinkとForeign Data Wrapper (FDW) が提供されています。
最近の方向性としては、FDWを充実させていくのが一般的な認識かと思います。

しかし、実際にデータベース連携を実現していく中で、FDWでは対応が困難なシーンがあります。
  • FDWを使って外部テーブルを実装する際に設定すべき項目が多い。
  • FDWは、VIEWのようにクエリを定義(固定)して使うため、アドホックな(もしくは動的に変わる)クエリのリモート実行ができない。
  • FDWのAPIは年々複雑化しており、もはや普通の開発者が気軽に拡張できるレベルでない。
  • そもそも、Cで開発できる or したい人はもうそんなにいない。
というわけで、このエントリではdblink(のサブセット)をPL/Pythonで再実装し、それを他のDBMSに対応させるために拡張する、ということを試みます。

PostgreSQLの強みのひとつは「拡張性の高さ」です。そのため、その「拡張性の高さ」を最大限に活かす実装を目指します。

なお、このPL/Pythonによるdblinkの再実装を、本エントリでは便宜上「dblink/py」と表記します。

■dblink/pyの実装方針


dblink/pyは以下の方針で実装します。
  • PostgreSQLのextensionとする。
  • APIはcontribのdblinkのサブセットとする。
  • 各DBMSへの接続、操作はPythonのDatabase APIに対応したドライバを活用する。
  • 気軽に拡張を行えるようにシンプルな実装に留める。
  • まずはPostgreSQLへのリモート接続機能を実装し、その後SQLiteへ拡張する。
このようにすることで、PL/Pythonによるdblinkのリファレンス実装とすることにしました。

■実装するdblink/pyのAPI


さて、dblink/pyで再実装するAPIを定義します。今回実装するAPIは以下の通りです。

以下は、リモートデータベースに接続してカーソルを操作するためのAPIです。
  • dblink_connect
  • dblink_disconnect
  • dblink_get_connections
  • dblink_open
  • dblink_fetch
  • dblink_close
以下は、カーソル操作ではなく、リモートのデータベースに接続してひとつのSQLを実行して接続を閉じる、という処理を行うAPIです。
  • dblink
  • dblink_exec
まずは、これくらいを実装できれば、リモートデータベースへの最低限の操作はできるだろう、という目論見です。

■dblink/pyの初期実装と動作確認


dblink/pyのコードは以下のレポジトリにあります。
初期実装(PostgreSQL接続のみサポートしたもの)は「release-pg」というブランチになっていますので、まずはこれをチェックアウトしてインストールします。

[snaga@devvm06 temp]$ git clone https://github.com/snaga/dblink_py.git
Cloning into 'dblink_py'...
remote: Enumerating objects: 78, done.
remote: Counting objects: 100% (78/78), done.
remote: Compressing objects: 100% (35/35), done.
remote: Total 78 (delta 36), reused 64 (delta 25), pack-reused 0
Unpacking objects: 100% (78/78), done.
[snaga@devvm06 temp]$ cd dblink_py/
[snaga@devvm06 dblink_py]$ git checkout release-pg
Branch release-pg set up to track remote branch release-pg from origin.
Switched to a new branch 'release-pg'
[snaga@devvm06 dblink_py]$ sudo env USE_PGXS=1 PATH=/usr/pgsql-10/bin:$PATH make install
/usr/bin/mkdir -p '/usr/pgsql-10/share/extension'
/usr/bin/mkdir -p '/usr/pgsql-10/share/extension'
/usr/bin/install -c -m 644 .//dblink_py.control '/usr/pgsql-10/share/extension/'
/usr/bin/install -c -m 644 .//dblink_py--0.1.sql  '/usr/pgsql-10/share/extension/'
[snaga@devvm06 dblink_py]$

インストールが完了したら、動作確認として2つのデータベース「testdb」と「testdb2」を作成して、dblinkを使って「testdb」を介して「testdb2」のデータを読み書きしてみます。

まず、データベースを作成します。
[snaga@devvm06 dblink_py]$ createdb -U postgres testdb
[snaga@devvm06 dblink_py]$ createdb -U postgres testdb2

次に「testdb」に接続して、dblink/pyをインストールします。

[snaga@devvm06 dblink_py]$ psql -U postgres testdb
psql (10.6)
Type "help" for help.

testdb=# create language plpython2u;
CREATE LANGUAGE
testdb=# create extension dblink_py;
CREATE EXTENSION

次に、「testdb」から「testdb2」に接続していることを確認します。
testdb=# select * from dblink('postgresql://localhost/testdb2', 'select current_database()', true) as (dbname text);
 dbname
---------
 testdb2
(1 row)

次に、CREATE TABLE ASを使ってtestdb2上にテーブルを作成し、そのテーブルの内容を読んでみます。
testdb=# select dblink_exec('postgresql://localhost/testdb2', 'create table temp as select current_database(),''ほげほげ''', true);
 dblink_exec
-------------
 SELECT 1
(1 row)

testdb=# select * from dblink('postgresql://localhost/testdb2', 'select * from temp', true) as (db text, foo text);
   db    |   foo
---------+----------
 testdb2 | ほげほげ
(1 row)

最後に、このテーブルがtestdb上には作成されておらず、testdb2上に作成されていることを確認します。
testdb=# \d
Did not find any relations.
testdb=# \c testdb2
You are now connected to database "testdb2" as user "postgres".
testdb2=# \d
        List of relations
 Schema | Name | Type  |  Owner
--------+------+-------+----------
 public | temp | table | postgres
(1 row)

testdb2=# select * from temp;
 current_database | ?column?
------------------+----------
 testdb2          | ほげほげ
(1 row)

以上で動作確認は完了です。(カーソルの動作確認はここではしてませんが、リグレッションテストには含まれていますので、 sql/dblink_py.sqlexpected/dblink_py.out を確認してみてください。)

■dblink/pyのSQLiteへの拡張


さて、動作確認ができたら、次はSQLiteに対応するようにdblink/pyを拡張してみます。

SQLite対応の拡張をしたコードは「release-sqlite」ブランチにあります。

dblink/py本体のコード(つまりテストコードを除く)に関しては、PostgreSQLのみ対応のバージョン(release-pgブランチ)と比べると、17行追加、2行削除されていることが分かります。
[snaga@devvm06 dblink_py]$ git checkout release-sqlite
Branch release-sqlite set up to track remote branch release-sqlite from origin.
Switched to a new branch 'release-sqlite'
[snaga@devvm06 dblink_py]$ git diff --stat release-pg dblink_py--0.1.sql
 dblink_py--0.1.sql | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)
[snaga@devvm06 dblink_py]$

つまり、この「プラス17行、マイナス2行」だけでSQLite対応が完了したということです。詳細な差分は以下を参照してください。
さて、それではSQLiteのデータベースに読み書きしてみます。

まず、/tmp/testdb3.db というSQLiteデータベース(ファイル)を想定して、テーブルを作成します。

testdb=# select dblink_exec('sqlite:///tmp/testdb3.db', 'create table t ( uid integer, uname text)', true);
 dblink_exec
-------------
 OK
(1 row)

次にそのテーブルが空であることを確認して、1件INSERT、再度SELECTしてレコードが挿入されたことを確認します。
testdb=# select * from dblink('sqlite:///tmp/testdb3.db', 'select * from t', true) as (uid int, uname text);
 uid | uname
-----+-------
(0 rows)

testdb=# select  dblink_exec('sqlite:///tmp/testdb3.db', 'insert into t values (1, ''aaa'')', true);
 dblink_exec
-------------
 OK
(1 row)

testdb=# select * from dblink('sqlite:///tmp/testdb3.db', 'select * from t', true) as (uid int, uname text);
 uid | uname
-----+-------
   1 | aaa
(1 row)

最後に、testdbデータベースにはテーブルが「作成されていないこと」を確認して、dblink/pyを通して読み書きしていたSQLiteデータベース /tmp/testdb3.db の内容を確認します。
testdb=# \d
Did not find any relations.
testdb=# \q
[snaga@devvm06 dblink_py]$ sudo -u postgres sqlite3 /tmp/testdb3.db
SQLite version 3.7.17 2013-05-20 00:56:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
t
sqlite> select * from t;
1|aaa
sqlite>

以上で、dblink/pyのSQLite対応の拡張とその動作確認は完了です。

■まとめ


というわけで、今回はdblinkの機能のサブセットをPL/Pythonで再実装し、それを他のDBMSに拡張する、ということを試してみました。

本家のcontribのdblinkはコードが3,000行以上ある大規模なモジュールですが、今回作ったdblink/pyのコードは200行以下です。SQLite対応の拡張に至っては、わずか20行以下のコードの変更で実現できています。PostgreSQLの拡張性をうまく活かすことで、これくらいの労力でさまざまな拡張ができる、ということがお分かりいただけたかと思います。

そんなこんなで今回いろいろやってきましたが、お伝えしたかったことは
  • 「FDWが無くてもdblinkを使ってVIEWを定義してしまえば見た目はFDWとだいたい一緒や」
  • 「シンプルに行こう。Less is more.」
ということです。

PostgreSQL Advent Calendar 2018、明日の担当は・・・なんと、未定です(本エントリ執筆時点)。

Advent Calendarが初日で終わってしまうのか。それとも奇跡が起きて継続されるのか。ドキドキハラハラが止まらない。刮目せよ。

では。

0 件のコメント:

コメントを投稿