読者です 読者をやめる 読者になる 読者になる

日曜プログラミング

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

datomic でハマった事いくつか

datomic でまだまだ遊んでいる所なのだがハマった事を順次書いていく。

DB は常に最新を見ましょう

結論から言うと DB 指定する所では過去を振り返る必要が出てくる時以外は常に (db conn) で最新の DB を指定すべきと言うお馬鹿なオチ。

話を単純化するためにここに気づいた経緯を mem プロトコルで attribute 一つ定義した simple-db で大体の流れを再現する。

(def uri "datomic:mem://simple-db") ; URI 指定

(require '[datomic.api :as d])
(d/create-database uri)         ; DB 作成
(def cn (d/connect uri))        ; 接続
(def db (d/db cn))              ; データ・ソース

; スキーマ定義
(def s-tx
  {:db/id #db/id[:db.part/db]
   :db/ident :foo/bar
   :db/valueType :db.type/string
   :db/cardinality :db.cardinality/one
   :db.install/_attribute :db.part/db})
; => #'user/s-tx

; 定義登録
@(d/transact cn s-tx)
; => {:db-before datomic.db.Db@b0a677a7, :db-after datomic.db.Db@f49877b5, :tx-data ...}

; 定義が入ったか確認。
; 指定した DB に定義があればデータ無しの空のSetが返ってくるがエラー
(d/q '[:find ?e :where [?e :foo/bar]] db)
; => IllegalArgumentExceptionInfo :db.error/not-an-entity Unable to resolve entity: :foo/bar  datomic.error/arg (error.clj:55)


; 上を無視してデータ登録
@(d/transact cn [[:db/add #db/id[:db.part/user] :foo/bar "baz"]])
; => {:db-before datomic.db.Db@f49877b5, :db-after datomic.db.Db@f3a1ba2f, :tx-data ...}

; データがあるか確認するがやはりエラー
(d/q '[:find ?e :where [?e :foo/bar]] db)
; => IllegalArgumentExceptionInfo :db.error/not-an-entity Unable to resolve entity: :foo/bar  datomic.error/arg (error.clj:55)


; ・・・あ、DB 指定が最初のままだった
(d/q '[:find ?e ?v :where [?e :foo/bar ?v]] (d/db cn))
; => #{[17592186045418 "baz"]}

と言うオチ。

属性値を更新する時はまずエンティティ ID を取る

さて、:foo/bar の値を更新したい時。

こうすると、

; :foo/bar を更新したくこうしてみたが、
@(d/transact cn [[:db/add #db/id[:db.part/user] :foo/bar "baz"]])
=> {:db-before datomic.db.Db@f3a1ba2f, :db-after datomic.db.Db@f2e0d6df, :tx-data ...}
(d/q '[:find ?e ?v :where [?e :foo/bar ?v]] (d/db cn))
=> #{[17592186045420 "baz"] [17592186045418 "baz"]}

このようにエンティティは異なるものの値が同じ attribute を持つものが複数出る。
これはすぐ分かるかもしれないが、:db/add で指定するエンティティ ID が #db/id[:db.part/user] と新たに ID 発行するよう指定しているのが原因。この現象を回避と言うのも語弊があり、datomic 本来の動作なのだ。

ある属性の値を変えたい場合はまずエンティティ ID を取得する。

; まず重複データを削除しておく
@(d/transact cn [[:db.fn/retractEntity 17592186045418]])
(d/q '[:find ?e ?v :where [?e :foo/bar ?v]] (d/db cn)) ; 確認
; => #{[17592186045420 "baz"]}

; エンティティ ID を取る
(def e-id (ffirst (d/q '[:find ?e :where [?e :foo/bar]] (d/db cn))))
@(d/transact cn [[:db/add e-id :foo/bar "fizz"]])
(d/q '[:find ?e ?v :where [?e :foo/bar ?v]] (d/db cn))
=> #{[17592186045420 "fizz"]}

もしくはまとめて登録する場合 attribute のスキーマ定義時に(datomicでは略して A V と呼んでたりしてる)ユニーク指定をしてやる方法もある。

;; 一旦 DB 丸ごと消して再度作る
(d/delete-database uri)
(d/create-database uri)
(def cn (d/connect uri))        ; 接続

; スキーマ定義・登録。今度は :db/unique を追加。指定値は今の所 :db.unique/identity のみ。
(def s-tx
  [{:db/id #db/id[:db.part/db]
    :db/ident :foo/bar
    :db/valueType :db.type/string
    :db/unique :db.unique/identity
    :db/cardinality :db.cardinality/one
    :db.install/_attribute :db.part/db}])
@(d/transact cn s-tx)

; 別エンティティIDで同じ属性に同じ値を 2 回追加
@(d/transact cn [[:db/add #db/id[:db.part/user] :foo/bar "baz"]])
@(d/transact cn [[:db/add #db/id[:db.part/user] :foo/bar "baz"]])

; unique 指定された属性に関しては同じ値の追加は許さない
(d/q '[:find ?e ?v :where [?e :foo/bar ?v]] (d/db cn))
; => #{[17592186045418 "baz"]}

確かドキュメントに SQL で言う DISTINCT 指定もできたとあった気がしたが試してない。SQL でもそうだが、単純な重複削除は JOIN させた時などに問題を見えにくくする事があるのであまり使いたくない。

これ敢えて別エンティティに同一属性を敢えて2つ持たせてやるような場面あるかな。。。ないか。
一つの属性に複数の値を持たせたい場合ならそれは :db/cardinality に many を指定してやるべきだと思う。