日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

datomic を試す(6) - データ操作、データ定義再び

datomic チュートリアルについては今回の記事で最終回。

前回は datomic の目玉の一つである時間の考え方について見てきた。
datomic を試す(5) - 時と共に - 日曜プログラミング

今回の記事で公式チュートリアルで該当する原文項目は以下。

  • Manipulating data
    • Transactions
    • Temporary ids
    • Adding, updating, and retracting data
  • Schema and seed data, revisited

原文にある通り、データ操作についてと、定義済みファイルに頼っていたデータ定義について改めて見ていく。

データ操作(Manipulating data)

トランザクション(Transactions)

トランザクションのデータ構造は単純なリスト or マップを要素とするリストらしい。

擬似コードでの例は以下のようなもの。

[:db/add entity-id attribute value]     ; 1 何かしらのエンティティの属性値を追加
[:db/retract entity-id attribute value] ; 2 何かしらのエンティティの属性値を差し戻し
[data-fn args*]                         ; 3 関数呼び出し

関数呼び出しって何?と言われると、例えば組み込みで retractEntity があり、これはエンティテ
ィ全体を差し戻す関数。

[:db.fn/retractEntity entity-id]

あるエンティティの属性値をまとめて変更する場合には Map 構造が使われる。

{ :db/id entity-id
  attribute value
  attribute value
  ... }

最初の :db/id にエンティティ ID を必須で指定する必要がある。後は属性名と値のペアを続けていく。

これは内部的にはリストに変換されるらしいが使う際に意識する事あるのかな。

テンポラリ ID(Temporary ids)

datomic の transaction においてエンティティ ID 指定は必須であり、実際更新・差し戻しをやる場合には次に示す方法でテンポラリ ID を発行する形になる。

その方法は 2 つあって、一つはリテラルライクに以下のように指定する方法。

#db/id[partition-name value*]

もう一つは Peer.tempid*1メソッドを実行する方法で、必要な引数はリテラルライク指定時と変わらず partition-name と任意で負数との事。

さて、自分にとってはパーティションって何よ、ってのが疑問だったが、この後を読むと何となく理解できた。どうもエンティティ ID を発行する単位のようなものっぽく、パーティション毎にキーワードの形で識別子が付けられている。

組み込みのパーティションもあり、以下表のものがある。

Partition 用途
:db.part/db システムパーティション、属性名や内部的な用途でのみ使われる
:db.part/tx transaction パーティション、 transaction エンティティのみに使われる
:db.part/user ユーザパーティション、 アプリケーション向け

この表を見るに、datomic システムのドメイン別にエンティティ ID は分けられているみたい。

通常は :db.part/user のみ使うべきだが、パーティション自体もエンティティである為、以下のように独自のパーティションを定義する事も可能。

[{:db/id #db/id[:db.part/db],
  :db/ident :communities,
  :db.install/_partition :db.part/db}]

データ追加・更新・差し戻し(Adding, updating, and retracting data)

データ追加

書式としてはこうすれば良いらしい。

[{:db/id #db/id[:db.part/user] :community/name "Easton"}]

実際に追加する前に :community/name が "Easton"のエンティティが本当にないのかが不安なので試してみる。

> (d/q '[:find ?e :where [?e :community/name "Easton"]] (d/db cn))
#{}

ないか。

では実際に追加・確認してみる。チュートリアルのこの項目では改めての説明がなく一瞬戸惑ったが、データの追加・更新を行うには transact を呼び出して DB 接続オブジェクトと追加するデータを指定する。

> (d/transact cn [{:db/id #db/id[:db.part/user] :community/name "Easton"}])
#<promise$settable_future$reify__5411@9053ac: {:db-before datomic.db.Db@1cc08267, :db-after datomic.db.Db@766fc0fe, :tx-data [#Datum{:e 13194139534685 :a 50 :v #inst "2013-11-07T00:06:33.427-00:00" :tx 13194139534685 :added true} #Datum{:e 17592186045790 :a 62 :v "Easton" :tx 13194139534685 :added true}], :tempids {-9223350046623220289 17592186045790}}>
> (d/q '[:find ?n :where [(fulltext $ :community/name "Easton") [[?e ?n]]]] (d/db cn))
#{["Easton"]}
データ更新

データの新規追加については前述したようにテンポラリ ID を発行する形となっていたが、既存のデータ更新に関してはテンポラリ ID に対してエンティティに付与されている ID を指定する事になる。

その為には更新をかけたいエンティティに対しての ID 取得が必要になる。
クエリとしては以下。

[:find ?belltown_id :where [?belltown_id :community/name "belltown"]]

まあここまで読み進めてくると想像はつくかも。

では実際に試してみる。ここでは belltown の :community/category を変更したいので変更前のカテゴリを確認しておく*2

> (d/q '[:find ?cat
         :where
          [?c :community/name "belltown"]
          [?c :community/category ?cat]]
    (d/db cn))
#{["events"] ["news"]}

んでコミュニティ名が "belltown" なエンティティ ID を取得。

> (def belltown-id (ffirst (d/q '[:find ?id :where [?id :community/name "belltown"]] (d/db cn))))
#'datomic-tut.core/belltown-id

取得したエンティティ ID を使って更新し、さっきと同じクエリで確認。

> (d/transact cn [{:db/id belltown-id  :community/category "free stuff"}])
#<promise$settable_future$reify__5411@12a0078: {:db-before datomic.db.Db@766fc0fe, :db-after datomic.db.Db@52ea7922, :tx-data [#Datum{:e 13194139534687 :a 50 :v #inst "2013-11-07T00:31:18.898-00:00" :tx 13194139534687 :added true} #Datum{:e 17592186045476 :a 65 :v "free stuff" :tx 13194139534687 :added true}], :tempids {}}>
> (d/q '[:find ?cat
         :where
         [?c :community/name "belltown"]
         [?c :community/category ?cat]]
    (d/db cn))
#{["events"] ["free stuff"] ["news"]}

belltown のカテゴリが追加されたのが分かる。

データ取り消し(retract)

最後にデータ取り消しの方法について。datomic では仕組みとして追加オンリーのシステムなので、DELETE などそのまま削除とは呼ばないようで、retract と表現してる。最初差し戻しとか言ってたが取り消しの方がまだ通じやすいかな。

やり方は更新と同じく実際のエンティティ ID を指定する、のだが内側が [[]] でなく {} なのが少し気になる。

[[:db/retract belltown_id :community/category "free stuff"]]

違うのは最初が :db/id でなく :db/retract になるというところだけ。

ではさっきカテゴリを更新した "free stuff" を取り消してみる。

> (d/transact cn [[:db/retract belltown-id  :community/category "free stuff"]])
#<promise$settable_future$reify__5411@a19599: {:db-before datomic.db.Db@52ea7922, :db-after datomic.db.Db@b057a2f0, :tx-data [#Datum{:e 13194139534688 :a 50 :v #inst "2013-11-07T00:41:44.727-00:00" :tx 13194139534688 :added true} #Datum{:e 17592186045476 :a 65 :v "free stuff" :tx 13194139534688 :added false}], :tempids {}}>
datomic-tut.core> (d/q '[:find ?cat
			 :where
			 [?c :community/name "belltown"]
			 [?c :community/category ?cat]]
		       (d/db cn))
#{["events"] ["news"]}

ちゃんと消えている。と言うよりは現時点の最新状況では見えなくなっていると言う感じで実データを消したわけではなく削除フラグが立てられたようなものと言うイメージ。過去を遡れば見ることは出来るんだろうが、ここでは改めての確認はしない。

また、これも未確認だが、retract 時に指定した属性の値が正確でない場合、その transaction は強制終了する。その為、transaction は改めて取得する必要があるとのこと。:db.fn/retractEntity を使って Entity 全体を取り消す場合は考慮する必要はないようだ。

データ定義・データ再び(Schema and seed data, revisited)

ここまで来ると確かにデータ定義も読めるようになってくる。恐らくこれを読んで試してる方は手元に読み込んだ時のファイルがあると思われるのでここで改めての掲載はしない。

ここで説明されている注意事項についていくつか。

  • 属性定義追加時に必要になるエンティティ ID 発行パーティションには :db.part/db を使う。
  • リテラルライクでの #db/id 指定によるデータ追加更新は文字列やファイルからの読込向けの為のもので、transaction をプログラマブルに行いたい場合は datomic.Peer.tempid を使うようにとの事。
  • 属性定義追加時には :db.install/_attribute も入れておく事。値には :db/id で指定したパーティションと同じものを指定する。
  • :region/ne などのある意味定数として扱うものは :db.part/user パーティションで ID 発行して追加している。

また、その他気づいたところと言うか気になるところと言うか。

  • 定数として定義したものを参照する属性 :community/type や :district/region などの値タイプは :db.type/ref となっているものの、定義上で明確に紐付いているわけではないのが少し気になる
  • :db.type/ref な属性の参照先の仮とも言うエンティティ ID が読み込んだファイルでは負数でリンクしていて、transactで確定登録された場合は適切なエンティティ ID になる、と言う所までは何となく分かるんだが、もうちょっと良い方法ないのかな

私的まとめ

元の公式チュートリアルは良く出来てると思う。特にクエリ中心に説明してるのは少なくとも日本語リソースではあまり見かけないので、ちゃんとした翻訳じゃないにせよこの記事を起こす動機になった。RDBMS 相手にした開発だと SQL ゴリゴリ書く場面も多いしやっぱりクエリ言語を把握するのは大事だとも思っているので。

ただ、チュートリアルだけでは RDBMS+SQL より便利になってるかどうかは正直自分はまだ良く分からない。新規プロジェクトならばパフォーマンスの良さそうなキーバリューストアを探してそいつをストレージとして組み合わせて使うのはいいのかも。自分としてはもっと使い込んでみないとまだ何とも。

*1:動作未確認の為ここではチュートリアルままの Java のメソッドの方を挙げた。とは言え Clojure API は多分 tempid になると思われる。

*2:せっかくの datomic なんだから時間で遡る方法取れば良いのかもしれないけどまだ慣れてない