Gauche:DBI/DBD

Gauche:DBI/DBD

(2005/07/03 15:35:57 PDT): 現在、別パッケージで提供されているDBI/DBDだが、特定のデータベースに 依存しない部分をGauche本体に取り込む。0.8.6に入る予定。

今のdbiのAPIは、当時のGaucheの機能が不十分だったこと(特に例外機構)や、 とりあえず使えることを優先して開発されたため、あまりすっきりしていない。 この機会にAPIを整理することにした。

実際にprepared statementのサポートを真面目に考え始めたら、 現状のdbiのアーキテクチャにかなり大幅な変更が必要になることがわかり、 結局中身はほとんど完全に入れ替わっている。

ただ、今までのdbiに依存しているアプリもあるし、ドライバの方もすぐには 新しいdbi APIに追従できないから、当面は古いAPIが混在した環境でも 動作させなければならない。


新API

ユーザレベルAPI

dbi-connect dsn &keyword username password ...

Perl::DBI式のデータソース名 dsn でドライバタイプとデータベース名などの オプションを指定 (e.g. "dbi:mysql:db=test;host=localhost")。

キーワード引数では、将来はautocommitの指定なども。

戻り値は<dbi-connection>のサブクラス。

dbi-prepare connection sql-statement &keyword pass-through ...

prepared statementの準備。戻り値は<dbi-query>もしくは そのサブクラス。

デフォルトではsql-statementに '?' によるパラメタライズが可能。

キーワード引数pass-throughが#tならパラメタライズを行わない。

他に、ドライバ独自のキーワード引数があるかも。

dbi-execute query parameter ...

dbi-prepareによって作られたqueryを実行する。queryがパラメータを 取る場合はparameter ... によって実パラメータを渡す。

戻り値は<relation>のサブクラス。

(let* ((query (dbi-prepare c "select * from tab where x = ? and y = ?")))
  (dbi-execute query "abc" "def")
  (dbi-execute query "ghi" "jkl")
  ...)

dbi-do connection sql-statement [options parameter ...]

prepareして即execute。options はdbi-prepareに渡されるキーワード引数の リスト。

dbi-escape-sql connection string

stringを安全な文字列リテラルへとエスケープ。

dbi-list-drivers

現在インストールされているドライバモジュール名のリストを返す。

ドライバレベルAPI

ドライバが定義すべきメソッド

dbi-make-connection driver attr-string attr-alist &keyword username password ...

ドライバによる定義:必須

dbi-connectから呼ばれる。

connectionを作って返す。attr-string はDSNの3番目のパート、 attr-alistはそれをパーズしてalistにしたもの。 キーワード引数はdbi-connectに渡されたもの。

旧APIのdbi-make-connectionとは引数の型で区別される。

dbi-prepare connection statement &keyword pass-through ...

ドライバによる定義:オプショナル

prepared statementの準備。<dbi-query>もしくはそのサブクラスを返す。

デフォルトメソッドはtext.sqlを使ってstatementをパーズし、 その情報を格納した<dbi-query>を返す。

ドライバによっては、この時点でバックエンドにstatementを送るものも あるかも。そういう場合はメソッドをオーバーロードする。

dbi-execute-using-connection connection query parameters

ドライバによる定義:必須

dbi-executeの下請け。dbi-executeを直接オーバロードしないのは、 ベースの<dbi-query>をそのまま使うドライバの場合ディスパッチができないから。

queryを発行する。それが結果を返す性質のもの(SELECT)ならば <relation>のサブクラスのオブジェクトを返す。そうでなければ 結果は未定義 (将来、何か有用な結果を定義するかもしれない)。

dbi-escape-sql connection string

ドライバによる定義:オプショナル

DBMSがエスケープ用関数を提供している場合はドライバがこのメソッドを定義すべき。 そうでない場合は "'" をエスケープするだけの処理が行われる。

その他

ドライバはdbi-doをオーバロードしてもよい。中間の<dbi-query>をスキップ できるので軽くなるかもしれない。


Legacy APIとの接続

APIの変更はアプリケーションとドライバ双方の変更を必要とする。 Gaucheのアップデートと同時にそれらをアップデートしてもらうのは 非現実的だ。Gaucheをアップデートしてdbi層だけが新しい状態でも 既存のアプリが動作しなければならず、またアプリケーションとドライバが 独立してアップデートできなければならない。すなわち、以下の組合せ全てが 動作する必要がある。

旧API

Legacy app + Legacy driver

この場合、旧APIの各メソッドの第一引数がより特定化されたメソッドが Legacy driverによって定義されている (Legacy driverは<dbi-driver>, <dbi-connection>, <dbi-query>をそれぞれサブクラスした独自のクラスを 定義しているため)。

従って、Legacy appが呼ぶdbi-make-connection(old)は新DBI層を バイパスして旧メソッドに直接制御が渡る。 dbi-make-driverとdbi-get-valueは新DBI層のメソッドが呼ばれるが、 これらは互換性がある。

Legacy app + New driver

新DBI層は旧APIのベースメソッドを次のように定義している。

(define-method dbi-make-connection ((d <dbi-driver>)
                                    (user <string>)
                                    (pass <string>)
                                    (options <string>))
  (dbi-connect #`"dbi:,(slot-ref d 'driver-name):,options"
               :username user :password pass))
(define-method dbi-make-query ((c <dbi-connection>) . _)
  (make <dbi-query> :connection c))
(define-method dbi-execute-query ((q <dbi-query>) (s <string>))
  (dbi-do (ref q 'connection) s))

New driverはこれらのメソッドをインターセプトすることはないので、 Legacy appからの旧APIメソッド呼び出しは新DBI層を経由して 新DBD APIのメソッドの呼び出しへとつながる。

New app + Legacy driver

新DBD APIであるdbi-make-connection(new)とdbi-execute-using-connectionのデフォルト メソッドは、旧DBD APIを呼ぶように作ってある。

(define-method dbi-make-connection ((d <dbi-driver>)
                                    (options <string>)
                                    (option-alist <list>)
                                    . args)
  (let-keywords* args ((username "")
                       (password ""))
    ;; call deprecated dbi-make-connection API.
    (dbi-make-connection d username password (or options ""))))

(define-method dbi-execute-using-connection ((c <dbi-connection>)
                                             (q <dbi-query>) params)
  (dbi-execute-query c (apply (ref q 'prepared) params)))

Legacy driverはこれらのメソッドを特定化しないので、旧DBD APIが 呼ばれることになる。


旧dbiからの変更

例外機構

ドライバ

コネクション

クエリー

外部表現を持たないものなら、S式QLの方が親和性がいいと思います。 <dbi-blob>みたいなオブジェクトを直接埋め込んでしまえるので。 Perl DBIとかはどうしてるんでしょ。

open/closed


議論

2005/9/3時点のCVS HEADについて

(katsujiroより移動) 早速CVSから落としてきておためし中です。Gauche本体もCVSで最新じゃないとダメなので結果結構時間がかかったです。

とりあえず、気になったところですが。こういうのはMLの方が良いのかしら。

Shiro: なるほど。まず「SQLをパーズしない」というオプションは dbi-prepare, dbi-do共通であった方がいいですね。標準のSQL以外の文法を 使いたい時などにそのままpass throughして欲しい場合もあるでしょうから。

それから、postgresqlのprepared queryについてですが、私が参考にした PerlのDBD:Pgはやはり '?' だけでパラメータを指定させます。 但しその時点でprepare statementは発行しません。最初のexecuteが行われる 時に、その引数からdatatypeを推測してprepareとexecuteを発行します。

dbi-prepareがクロージャを返すというのは良いアイディアですね。 確かにdbi-executeがメソッドでなければならない理由というのが 思い付きません。特に障害がなさそうならそうしてしまおうかと思います。

Shiro(2005/09/07 05:03:06 PDT): というわけで、dbd-executeは無しにして、 dbi-prepareがクロージャを返すように変更しました。 ドライバAPIもこっちの方がわかりやすいと感じます。

katsujiro(2005/09/07 20:41:19 PDT): 確認しました。やっぱ、クロージャとか返すのってschemeっぽくて良いですね。

prepareのパラメータの型の問題はいろいろ考えてしまいます。SQL上でquoteで囲むものは、自動的に囲んで欲しいと思うので、そう言うdbdのpgをちょっと書いてみていますが、パラメータの型を自動判別って結構難しいです。でも、自動型変換をあてにして、型はTEXTとして決めうちし、executeする際にはパラメータを全部escapeしてquoteしてから渡せば、動く分には問題無さそうです。(最適化という意味では良くないかも知れませんが。)試しに作ってみてます。

余談ですが、92行目のmessageは(pq-result-error-message result)でしょうか。

Shiro(2005/09/10 05:18:14 PDT): dbi-prepareはドライバ依存のキーワード引数を取れるので、 どうしてもdatatypeを明示したい向きには:pg-datatypes みたいなキーワード引数で それを与えてやる、という方向もありだと思います。

Shiro(2005/09/11 01:02:29 PDT): しまった。dbi-prepareが単なるクロージャを返す場合、 prepared statementに割り当てられたリソースを明示的に開放する手段がありません。 うーんどうしよう。開放の必要なリソースは<dbi-connection>の方に登録 しといてもらって、(dbi-close <dbi-connection>) で一気に開放する、という 手はありますが、それで大丈夫なんだろうか。サーバプロセスでconnection張りっぱなし とかだとまずいしな。

katsujiro(2005/09/12 06:56:29 PDT): 私なりにもいろいろ考えたんですけど、やっぱり<dbi-prepared>にクロージャとリソース情報を格納するしか無いと思います。どうしてもクロージャでやりたいということで、クロージャに渡す値によって、クロージャに始末させるということも考えたのですが、スマートではありません。

Shiro(2005/09/12 21:33:27 PDT): 結局、dbi-prepareが<dbi-query>を返し、それを dbi-executeで実行する、というふうに戻しました。

(2005/09/22 22:20:11 PDT): あれ、旧APIにあった<dbi-result-set>ってなくなっちゃいましたか?

Shiro(2005/09/22 23:38:21 PDT): 新APIにはないです。各ドライバが勝手に<relation>を 継承してresult setを表現して下さい。<dbi-result-set>が無いと 動かないlegacy codeがあるなら互換レイヤとして0.8.6には残すようにします。 (でも、共通のベースクラスがあった方が便利なケースがあれば復活させます)

(2005/09/23 03:53:41 PDT: Gauche-firebirdのDBD部分が動かなくなったのですが、いい機会なので新APIに対応させることにします。わたし個人は特に残す必要はないです。

Shiro(2005/10/06 06:42:26 PDT): しまったぁぁぁ。0.8.6リリースに向けて 旧ドライバとの互換性チェックをしてたんだが、dbi-escape-sqlの仕様が 非互換なことに気付いた。新dbiの方ではdbi-escape-sqlは渡された文字列の 先頭と末尾にクオートを付加しない。 caller側でescapeした文字列をくっつけたりする場合にその方が 都合がいいから。dbd-mysqlとdbd-pgの0.1.6ではクオートを付加してしまう。 うーん… 互換性無くなって困る人、どのくらいいます?

えんどう(2005/10/12 20:35:58 PDT)あれ?新dbiを見てそのまま真似したつもりだったのに。なんか勘違いしてましたか。kahua.persistenceでdbi-escape-sqlを使ってますがGauche 0.8.6リリース後に新dbiに対応します。特に問題ないと思います。

relationはcollection? sequence?

katsujiro:result setがrelationを継承するということですが、relationはrowのcollectionですよね。っていうことは、result setのrowには順序関係が無い?RDBのrelation自体には内部で順序関係はないわけですが、SQLのクエリではORDER BYでソートし、順序を付けて結果を取り出すことが出来るので、結果はsequenceかなと思うのですが。 collectionでもイテレータは頭から順番に取り出してくるので、現状は問題ないのですが

Shiro (2005/11/02 04:48:04 PST): ああしまった。Coddの原論文見てるうちにORDER BYのことを すっかり忘れてました。ううむ。ハッシュテーブルを<relation>と看做すとか、 モノによっては<sequence>でなくても良い場合もあるんですよねぇ。 <relation>が<collection>/<sequence>を継承すべきではないのか? <relation-mixin>にしてユーザクラスでmultiple inheritanceするか。

katsujiro(2005/11/02 23:16:46 PST):私の使い方だけで考えると(;-)少なくともRDBとCSVでは<sequence>が適しているように思えます。どちらも順序関係があるか無いか、取得したデータを見ただけでは区別が付かないので、そのデータの順序関係を尊重せざるを得ないと思います。SQLカーソルをresult-setに隠蔽することも<sequence>
DBD::Anydataみたいなのを考慮されているのでしたら、あれもSQLでアクセスするわけですから、結果はsequencialなのかなとも思いますし。
result setからさらに何らかのindexで検索できる必要は無いと思います。あらかじめ、検索してから取得した結果なので、線形探索で十分な大きさに絞り込まれているという前提で考えても良いのではないかと。
もし、result setから特定のcolumnをキーにしたhashなりtreeなりが欲しければ、sequenceの行数を値にしたlookup tableを作れば良いと思いますし。

Shiro(2005/11/02 23:58:26 PST): <sequence>の特質はinteger indexableであることなんで、 そうでない<collection>をベースに<relation>を作ろうとした時に、 <sequence>の継承は邪魔になります。こういうことで悩む場合は大抵、 無理に継承関係を作らない方が良いことになるんで、こんな感じでどうでしょうかね:

Shiro(2005/11/03 04:49:58 PST): 変えてみました。Gauche-dbd-{pg,mysql}も併せて 変更してみました。意外とクライアントコードに及ぼす影響は少なそうなので、 これでいけるかもしれません。ドキュメントはまだ書き直してません (一番変更が大きいのがドキュメントかも)。

katsujiro(2005/11/03 08:30:14 PST):さっそく持ってきて試してみました。
dbd/pg.scmの138行目の=<は<=ですね。result setが<sequence>として動いていて使えるのを確認しました。ありがとうございます。
余談ですが、

(define-method dbi-affected-rows ((r <pg-result-set>))
  (pq-cmd-tuples (slot-ref r '%pg-result))) ;;; pgの場合

こういうのが欲しいです。result-setのvirtual-slotでも良いのですが。

dbd.pgのdbi-column-count

0.8.7pre1 CVSHEAD(2006.03.01時点)です。

dbd.pgの46行目にdbi-column-countという便利なアクセサが用意されているのに、exportされていないためか、使えないのが残念です。katsujiro

More ...