Common Lispのデータベースライブラリというか、O/Rマッパーとしては3ヶ月前に僕が作ったIntegralがあります。
IntegralはCLOSやMOPなどCommon Lispの魔術を余すこと無く使い、拡張性や高度なマイグレーション機能もあるライブラリとして他の追随を許しません。
ただ、すべてのアプリケーションでO/Rマッパーのような機能が必要なわけではないでしょう。抽象化レイヤーを薄く保って、極力コントローラブルにしたいという要望もあります。
今回紹介する「datafly」はそういった要求を満たす軽量なDBライブラリです。
dataflyの思想
一般的なO/Rマッパーでは、データベースの「テーブル」と、プログラム言語の「クラス定義」が一対一対応しています。この大きな前提のおかげでデータベースを抽象化でき、まるでクラス定義が(半)永続化しているように錯覚させてくれます。
ただし、そこにはトレードオフがあります。
O/Rマッパーはその性質上データベースやSQL発行を表向き見えなくするものなので、コストのかかるSQL発行が行われているときに気づきづらくなります。
その点、dataflyは逆の思想に基づいています。
dataflyでは暗黙のSQLの発行を行いません。マクロを除く黒魔術は使いません。透明性を重視し、アプリケーションごとの最適化を行いやすくコントローラブルな状態に保ちます。
機能
上述の通り、dataflyはO/Rマッパーではありません。たとえば、dataflyは以下のようなことはしません。
- クラス定義からDBスキーマの生成はしません。
- DBスキーマからクラス定義の生成もしません。
- インスタンスの変更を感知してUPDATE/DELETE文を発行する機能はありません。
- マイグレーション機能はありません。
dataflyがやるのはこんなことです。
- DBコネクション管理
- DBへのクエリ発行
- 結果を構造体(Structure)にマッピング
- inflate
CLOSのクラスではなく構造体を使うのでいくらか効率も良いはずです。
クイックスタート
構造体(Structure)へのマッピング
dataflyではSQLの発行方法としてretrieve-one
、retrieve-all
、execute
の3つの関数があります。すべて引数としてSxQLのクエリオブジェクトを受け取ります。
たとえば、SELECT文を投げて、結果を1つ返して欲しいときはretrieve-one
を使います。
(retrieve-one (select :* (from :user) (where (:= :name "nitro_idiot")))) ;=> (:ID 1 :NAME "nitro_idiot" :EMAIL "nitro_idiot@example.com" :REGISTERED-AT "2014-04-14T19:20:13")
返り値はプロパティリストです。
キーワード引数の:as
を指定すると、結果を指定した構造体(Structure)として返します。
(defstruct user id name email registered-at) (retrieve-one (select :* (from :user) (where (:= :name "nitro_idiot"))) :as 'user) ;=> #S(USER :ID 1 :NAME "nitro_idiot" :EMAIL "nitro_idiot@example.com" :REGISTERED-AT "2014-04-14T19:20:13") (user-name *) ;=> "nitro_idiot"
この例ではテーブル名と構造体の名前が同じですが、同じである必要は全くありません。dataflyはテーブルとクラスが一対一対応ではないからです。
この自由度は、架空のテーブル――たとえばJOINした結果――などを構造体として扱いたいときなんかに便利です。
;; "user_bank"という名前のテーブルは存在しない。 (defstruct user-bank user-id name bank-balance) (retrieve-one (select (:user_id :name (:as (:sum (:amount)) :bank_balance)) (from :user) (left-join :bank_transactions :on (:= :user.id :bank_transactions.user_id)) (where (:= :name "nitro_idiot"))) :as 'user-bank) ;=> #S(USER-BANK :USER-ID 1 :NAME "nitro_idiot" :BANK-BALANCE 200000)
いずれの例でも、結果として返ってきた構造体オブジェクトにsetf
で変更を加えてもO/Rマッパーのようにデータベースに更新処理が行えるわけではありません。あくまでデータベースから構造体への一方向のマッピングだけを行います。
モデル定義としての構造体
少しずつ複雑な例を紹介していきます。
上の例では単なるCommon Lispの構造体を使いました。
これだけで十分な方も多いかもしれませんが、dataflyでは少し変わった構造体を定義する機能もあります。
使い方は簡単です。defstruct
の代わりにdefmodel
というマクロを使います。
(defmodel user id name email registered-at)
アノテーションライブラリのcl-annotを使うと@model
と書くこともできます。
(annot:enable-annot-syntax) @model (defstruct user id name email registered-at)
以下では@model
を使うものとします。
inflate
@model
をつけると構造体定義にいくつかの特殊なオプションをつけることができます。
その一つが:inflate
です。
@model (defstruct (user (:inflate registered-at #'datetime-to-timestamp)) id name email registered-at)
(:inflate <カラム> <関数>)
を記述すると、オブジェクトを作るときに指定した<カラム>
の値に<関数>
を自動適用します。この例ではregistered-at
というカラムをLOCAL-TIMEのTIMESTAMPオブジェクトに変換します。
(defvar *user* (retrieve-one (select :* (from :user) (where (:= :name "nitro_idiot"))) :as 'user)) ;; Returns a local-time:timestamp. (user-registered-at *user*) ;=> @2014-04-15T04:20:13.000000+09:00
:inflate
は複数つけることもできます。また、<カラム>
の部分をリストにして複数のカラムを指定することもできます。
オブジェクトからデータベースにINSERT/UPDATE/DELETE文を発行する機能は無いので、反対の:deflate
はありません。
:has-a と :has-many
他に指定できるオプションとして:has-a
と:has-many
があります。これらはテーブルのカラムの関係性を定義することで、構造体にアクセサを追加する機能です。
@model (defstruct (user (:inflate registered-at #'datetime-to-timestamp) (:has-a config (where (:= :user_id id))) (:has-many (tweets tweet) (where (:= :user_id id)) (order-by (:desc :created_at)))) id name email registered-at) (defstruct config id user-id timezone country language) (defstruct tweet id user-id body created-at)
この例ではuser-config
とuser-tweets
というアクセサが自動で定義されます。
(defvar *user* (retrieve-one (select :* (from :user) (where (:= :name "nitro_idiot"))) :as 'user)) (user-config *user*) ;=> #S(CONFIG :ID 4 :USER-ID 1 :TIMEZONE "JST" :COUNTRY "jp" :LANGUAGE "ja") (user-tweets *user*) ;=> (#S(TWEET :ID 2 :USER-ID 1 :BODY "Is it working?" :CREATED-AT @2014-04-16T11:02:31.000000+09:00) ; #S(TWEET :ID 1 :USER-ID 1 :BODY "Hi." :CREATED-AT @2014-04-15T18:58:20.000000+09:00))
:has-a
や:has-many
で定義されたアクセサを呼び出すとSELECT文が発行されることに注意してください。
結果は初回でキャッシュされるので、二度以上呼び出しても何回もクエリが発行されるわけではないので安心してください。
おわりに
Integralと違ってブログポスト一つでほとんどの機能が紹介できてしまった。JSON吐くだけのWeb APIサーバとかならこの程度で十分ですね。
今回作ったdataflyはGitHubで公開しています。
また、来週の火曜の夜は渋谷でLisp Meetupがあります。興味がある方はどうぞご参加ください。
Lisp Meet Up presented by Shibuya.lisp #16 : ATND
日時: 4/22(火) 19:30 〜 21:30
場所: 渋谷マークシティ ウエスト13階 セミナールーム