本エントリは、 PostgreSQL Advent Calendar 2018の Day1 のエントリです。
エントリを書くのは実に半年以上ぶりなのですが、今回は以前から試してみたかったdblinkネタをお届けします。
PostgreSQLには、PostgreSQL、あるいは異種DBMSのデータベース連携を実現する手段として、dblinkとForeign Data Wrapper (FDW) が提供されています。
しかし、実際にデータベース連携を実現していく中で、FDWでは対応が困難なシーンがあります。
PostgreSQLの強みのひとつは「拡張性の高さ」です。そのため、その「拡張性の高さ」を最大限に活かす実装を目指します。
なお、このPL/Pythonによるdblinkの再実装を、本エントリでは便宜上「dblink/py」と表記します。
dblink/pyは以下の方針で実装します。
さて、dblink/pyで再実装するAPIを定義します。今回実装するAPIは以下の通りです。
以下は、リモートデータベースに接続してカーソルを操作するためのAPIです。
dblink/pyのコードは以下のレポジトリにあります。
初期実装(PostgreSQL接続のみサポートしたもの)は「release-pg」というブランチになっていますので、まずはこれをチェックアウトしてインストールします。
インストールが完了したら、動作確認として2つのデータベース「testdb」と「testdb2」を作成して、dblinkを使って「testdb」を介して「testdb2」のデータを読み書きしてみます。
まず、データベースを作成します。
次に「testdb」に接続して、dblink/pyをインストールします。
次に、「testdb」から「testdb2」に接続していることを確認します。
次に、CREATE TABLE ASを使ってtestdb2上にテーブルを作成し、そのテーブルの内容を読んでみます。
最後に、このテーブルがtestdb上には作成されておらず、testdb2上に作成されていることを確認します。
以上で動作確認は完了です。(カーソルの動作確認はここではしてませんが、リグレッションテストには含まれていますので、 sql/dblink_py.sqlと expected/dblink_py.outを確認してみてください。)
さて、動作確認ができたら、次はSQLiteに対応するようにdblink/pyを拡張してみます。
SQLite対応の拡張をしたコードは「release-sqlite」ブランチにあります。
dblink/py本体のコード(つまりテストコードを除く)に関しては、PostgreSQLのみ対応のバージョン(release-pgブランチ)と比べると、17行追加、2行削除されていることが分かります。
つまり、この「プラス17行、マイナス2行」だけでSQLite対応が完了したということです。詳細な差分は以下を参照してください。
さて、それではSQLiteのデータベースに読み書きしてみます。
まず、/tmp/testdb3.db というSQLiteデータベース(ファイル)を想定して、テーブルを作成します。
次にそのテーブルが空であることを確認して、1件INSERT、再度SELECTしてレコードが挿入されたことを確認します。
最後に、testdbデータベースにはテーブルが「作成されていないこと」を確認して、dblink/pyを通して読み書きしていたSQLiteデータベース /tmp/testdb3.db の内容を確認します。
以上で、dblink/pyのSQLite対応の拡張とその動作確認は完了です。
というわけで、今回はdblinkの機能のサブセットをPL/Pythonで再実装し、それを他のDBMSに拡張する、ということを試してみました。
本家のcontribのdblinkはコードが3,000行以上ある大規模なモジュールですが、今回作ったdblink/pyのコードは200行以下です。SQLite対応の拡張に至っては、わずか20行以下のコードの変更で実現できています。PostgreSQLの拡張性をうまく活かすことで、これくらいの労力でさまざまな拡張ができる、ということがお分かりいただけたかと思います。
そんなこんなで今回いろいろやってきましたが、お伝えしたかったことは
PostgreSQL Advent Calendar 2018、明日の担当は・・・なんと、未定です(本エントリ執筆時点)。
Advent Calendarが初日で終わってしまうのか。それとも奇跡が起きて継続されるのか。ドキドキハラハラが止まらない。刮目せよ。
では。
エントリを書くのは実に半年以上ぶりなのですが、今回は以前から試してみたかったdblinkネタをお届けします。
■なぜ今さら「dblink」?
PostgreSQLには、PostgreSQL、あるいは異種DBMSのデータベース連携を実現する手段として、dblinkとForeign Data Wrapper (FDW) が提供されています。
- https://www.postgresql.jp/document/10/html/dblink.html
- https://www.postgresql.jp/document/10/html/postgres-fdw.html
しかし、実際にデータベース連携を実現していく中で、FDWでは対応が困難なシーンがあります。
- FDWを使って外部テーブルを実装する際に設定すべき項目が多い。
- FDWは、VIEWのようにクエリを定義(固定)して使うため、アドホックな(もしくは動的に変わる)クエリのリモート実行ができない。
- FDWのAPIは年々複雑化しており、もはや普通の開発者が気軽に拡張できるレベルでない。
- そもそも、Cで開発できる or したい人はもうそんなにいない。
PostgreSQLの強みのひとつは「拡張性の高さ」です。そのため、その「拡張性の高さ」を最大限に活かす実装を目指します。
なお、このPL/Pythonによるdblinkの再実装を、本エントリでは便宜上「dblink/py」と表記します。
■dblink/pyの実装方針
dblink/pyは以下の方針で実装します。
- PostgreSQLのextensionとする。
- APIはcontribのdblinkのサブセットとする。
- 各DBMSへの接続、操作はPythonのDatabase APIに対応したドライバを活用する。
- 気軽に拡張を行えるようにシンプルな実装に留める。
- まずはPostgreSQLへのリモート接続機能を実装し、その後SQLiteへ拡張する。
■実装するdblink/pyのAPI
さて、dblink/pyで再実装するAPIを定義します。今回実装するAPIは以下の通りです。
以下は、リモートデータベースに接続してカーソルを操作するためのAPIです。
- dblink_connect
- dblink_disconnect
- dblink_get_connections
- dblink_open
- dblink_fetch
- dblink_close
- 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.sqlと expected/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が初日で終わってしまうのか。それとも奇跡が起きて継続されるのか。ドキドキハラハラが止まらない。刮目せよ。
では。