日曜プログラミング

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

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")

ちょっと引っかかる事

  • 記事中でも書いたがデータ定義でのコロン(:)とドット(.)の使い分けの意味合いって何だ
  • お隣さんのコミュニティ名全取得の Clojure コードがこれで良いのかちょっと自信なし
    • 同じのが複数出るとか
    • 結果の要素がコミュニティ名の文字列じゃなく、0以上(?)の隣接地域名を持つシーケンスになるんじゃないかとか
  • 最後の Clojure コードも Java の結果と異なるが、Map データだから多分最初の実体が何になるのかという保証がないんだろな
    • ソートをどこに挟み込むかってのは注意する必要があるかも

今日はここまで。
公式見ると分かるけど結構チュートリアル長いんだよね。

*1:集合論で出てくる用語と全く同じかどうかは分からないが、datomic では 1 対 1 か 1 対 n かを定義できる項目としての意味合いっぽい

*2:原文が their values must be unique across entities なのだがまだ良く分からん

*3:何かややこしいけどポインタのポインタみたいな感じか