datomic チュートリアルを試す(2) - datomic のデータ定義・実体の属性値の取得
前回、インストール・DB作成・スキーマ定義・クエリ実行までやってみた。記事はこちら。
datomic チュートリアルを試す(1) - 導入・DB 作成・最初のクエリ実行 - 日曜プログラミング
また、この記事書いてる途中で作者による Clojure でどう書くかの動画があるのに気づいた。
Writing Datomic in Clojure
まだ動画は見てないが英語が分かれば作者なだけに正確な説明してくれてるんじゃないかと。
前回ではあらかじめチュートリアル用に用意された定義ファイルを読み込んだだけなので今回はそもそもデータ定義ってどうなってるのと言う所ともう少し進んだデータの取得の仕方を見て行きたいと思う。
今回公式チュートリアルで該当する原文項目は以下。
- Adding a schema
- Getting an entity's attribute values
そのまま翻訳するわけではないが、元の英語の用語やチュートリアルの項目は併記する。また、前回からそうだがチュートリアルを上からなぞってはいないので悪しからず。
datomic の schema(Adding a schema)
最低限の構成要素
schema の詳細についてはひとまず置いといて、最初に以下の事については覚えておいて欲しいと。
- schema は実体(entity)に当てはまる属性名(attribute's name)のセットを定義する
- 属性名は内部的には :id/ident と呼ばれる
- 属性は名前(name)、タイプ(type)、濃度(cardinality)を決める*1
あと属性としては他にも実体間での値がユニークである*2とか、全文検索インデックスを生成するかどうかなどを決められるらしい。
また、読み込んだサンプルデータにはシアトルのコミュニティ(communities)、隣接地域(neighborhoods)、地方(districts)を表すデータがある。慣例的に、特定の実体を表す属性にはプレフィックスが付けられる。
公式に書いてある属性一覧をこちらに再掲する。プレフィックスがどこに当たるのかはこれで分かるかと。
Attribute | Type | Cardinality |
---|---|---|
:community/name | String | One |
:community/url | String | One |
:community/neighborhood | Reference | One |
:community/category | String | Many |
:community/orgtype | Reference | One |
:community/type | Reference | One |
:neighborhood/name | String | One |
:neighborhood/district | Reference | One |
:district/name | String | One |
:district/region | Reference | One |
表を見ると :community/neighborhood や :neighborhood/district などのタイプが Reference になっている。これはこの属性の値(value)は他の実体を参照する事を意味している。
ここでは community と neighborhood、neighborhood と district とを関連付けるのに使われている。ちなみにファイル読込時に実体も読み込まれている。
ここでちょっと RDBMS のデータの持ち方と比較
ここはまるっきり自分の解釈です。自分もまだチュートリアル読み進めてる途中で間違ってる可能性があるので混乱したくない人は次の項目までスキップして下さい。
datomic では実体(entity)はありのままの素のデータで、それ自身はデータ以外に何も持っているわけではない。その実体が何を意味するのかと言うのを、実体とは分離した属性(attribute) というものにヒモ付けると言う感じか。これを datomic 紹介時に言っている datom とか事実(fact)と呼んでいるのかな。datom と fact を分けているのは何かしら意味がありそうだけどここでは置いとく。
対して RDBMS では先にテーブルと言うデータ構造を定義してそこに実データを入れ込む為本質的に実体と属性と言うものは切り離せない。
datomic では実体は既に存在するもので、そこに属性を付けていくイメージになるかな?
RDBMSではテーブル定義と言う箱ありきで、そこにデータを入れると属性が決定されると言うか。
何か Clojure と言うか Lisp に通ずるものがあるな。REPL で 1 と評価したら 1 と返ってくるようなのを entity と指すのかね。
あ、だから value と entity と分けてたのかな?
他の Reference タイプについて
先の表で :community/orgtype や :community/type 属性のタイプも Reference となっていたが、これらは他の列挙された値を参照する為の名前を定義している*3
以下の表にあるように、:community/orgtype や :community/type 属性はキーワード識別子の形で定義されてる。
Reference attribute | Enumerated value identifier |
---|---|
:community/orgtype | :community.orgtype/community |
:community.orgtype/commercial | |
:community.orgtype/nonprofit | |
:community.orgtype/personal | |
:community/type | :community.type/email-list |
:community.type/twitter | |
:community.type/facebook-page | |
:community.type/blog | |
:community.type/website | |
:community.type/wiki | |
:community.type/myspace | |
:community.type/ning | |
:district/region | :region/nw |
:region/n | |
:region/ne | |
:region/e | |
:region/se | |
:region/s | |
:region/sw | |
:region/w |
コロン(:)とドット(.)の使い分けの意味合いが気になるがまあとりあえず先へ。
また、公式ではこの後データ読込の方法と最初のクエリ実行にについて書かれてるが、自分は一足先に前回やっているので飛ばす。
実体の属性値を取得する(Getting an entity's attribute values)
ちょっと前回のクエリと実行結果を再掲する。
> ; 前仕込み。transactor 起動しておくか mem プロトコルで読込を済ませとく事。 > (def cn (d/connect "datomic:free://localhost:4334/seattle")) > (def result (q '[:find ?c :where [?c :community/name]] (db cn))) > ; クエリ実行 > (q '[:find ?c :where [?c :community/name]] (db cn)) #{[17592186045520] [17592186045518] [17592186045517] [17592186045516] ...(省略)...}
最初結果の意味が分からんかったが、:community/name 属性を持つ実体を返すような感じか。なので、結果の数を数えればコミュニティ全体数が分かると言う解釈が可能。
で、一旦このクエリ結果を result に入れておく。
> (def result (q '[:find ?c :where [?c :community/name]] (db cn))) result
クエリの実行結果は一見 Clojure の set のように見えるが、
> (type result) java.util.HashSet
実際は Java のオブジェクト。
このクエリ結果の先頭を取って :community/name の値を返すにはこう。
; 最初に取得した実体のコミュニティ名を取得 > (-> (d/entity (d/db cn) (ffirst result)) :community/name) "Downtown Dispatch"
ちなみに公式の Java でのサンプルだとこうなっている。
id = (results.iterator().next()).get(0); entity = conn.db().entity(id); name = entity.get(":community/name");
またスレッドマクロ最初の結果について補足。
> (d/entity (d/db cn) (ffirst result)) {:db/id 17592186045520}
これも一見単なる Clojure の map に見えるが、
> (type (d/entity (d/db cn) (ffirst result))) datomic.query.EntityMap
なのでこれまた注意。
Clojure に最適化されてるわけではなく、そこそこ Java である事を意識する必要がありそう。とは言え、HashSet やら Java の Map は Clojure では割と簡単に扱えるのでいいかも。
閑話休題。
結果セットから全てのコミュニティ名が欲しい時はこう。
; 全てのコミュニティ名を取得 (map #(->> (first %) (d/entity (d/db cn)) :community/name) result) ("Downtown Dispatch" "Discover SLU" ... 省略 ... "Greenwood Community Council Discussion" "Haller Lake Community Club" "Hawthorne Hills Community Website") nil
何かもうちょい簡単に書ける気がするがそれはさておき、比較用にチュートリアルにある元の Java コードはこちら。
for (Object result : results) { entity = db.entity(result.get(0)); System.out.println(entity.get(":community/name")); } // 結果は省略
最初に取得した実体の隣接地域の名前一覧を取得するにはこう。
; 最初に取得した実体のお隣さんのコミュニティ名を全取得 > (map #(->> (first %) (d/entity (d/db cn)) :community/neighborhood :community/name) result) ("Downtown" "South Lake Union" "South Lake Union" "South Lake Union" "Delridge" "Delridge" "Delridge" ... 省略 ... "Greenwood" "Greenwood" "Greenwood" "Greenwood" "Haller Lake" "Hawthorne Hills")
で、次のチュートリアルの説明だが、何でそんな事できんだ?ってのが良く分からんけど簡単に訳する。
datomic でのリレーションは双方向です。言い換えると、community の :community/neighborhood
属性は community entity と neighborhood entity のどちらからもヒモ付け可能なリレーションを
生成します。neighborhood entity に :neighborhood/community 属性がないにも関わらず、です。
具体的には属性名(Reference attribute)のプレフィックス後の先頭に _ を付加すると逆方向のリレーションを参照できます。
以下が逆リレーションとでも言えばいいのかな、それを使ったサンプル。
; 最初の実体のお隣が隣接するコミュニティ名を取得 > (map :community/name (->> (ffirst result) (d/entity (d/db cn)) :community/neighborhood :community/_neighborhood)) ("Downtown Seattle Association" "Downtown Dispatch" "KOMO Communities - Downtown")