ここ最近書いていたDatomicチュートリアルのあれは、結構フォーマルな文体で書いていましたが、このエントリーはかなりカジュアルに書きます。
自己参照アトリビュート
datomic tutorial 15日目で書いたように、参照型はとにかくなんでもエンティティを参照することができます。すると、そのエンティティ自身を参照するアトリビュートを作ることもできるのかどうか試してみたいと思うようになります。
そこで、次のようなスキーマの定義をしてみます。
エンティティ | アトリビュート | 型 | カーディナリティ | 特記 |
---|---|---|---|---|
person | name | string | one | - |
person | sex | ref(enum) | one | :person.sex/woman, :person.sex/man |
person | birth | long | one | - |
person | self | ref(entity) | one | 自信への参照 |
スキーマ定義は次のようになります。
[ {:db/id #db/id[db.part/db -1000000], :db/ident :person/name, :db/valueType :db.type/string, :db/cardinality :db.cardinality/one, :db.install/_attribute db.part/db} {:db/id #db/id[db.part/db -1000001], :db/ident :person/sex, :db/valueType :db.type/ref, :db/cardinality :db.cardinality/one, :db.install/_attribute db.part/db} {:db/id #db/id[db.part/db -1000002], :db/ident :person/birth, :db/valueType :db.type/long, :db/cardinality :db.cardinality/one, :db.install/_attribute db.part/db} {:db/id #db/id[db.part/db -1000003], :db/ident :person/self, :db/valueType :db.type/ref, :db/cardinality :db.cardinality/one, :db.install/_attribute db.part/db} {:db/id #db/id[:db.part/user -1000004], :db/ident :person.sex/woman} {:db/id #db/id[:db.part/user -1000005], :db/ident :person.sex/man} ]
さて、上記のスキーマに次のようなデータを投入します。
:person/name | :person/sex | :person/birth | :person/self |
---|---|---|---|
山田太郎 | :person.sex/man | 1995 | 自分自身 |
山本華子 | :person.sex/woman | 1997 | 自分自身 |
このデータは次のトランザクションを発生させます。
[ {:person/name 山田太郎, :person/sex :person.sex/man, :person/birth 1995, :person/self #db/id[:db.part/user -1000006], :db/id #db/id[:db.part/user -1000006]} {:person/name 山本華子, :person/sex :person.sex/woman, :person/birth 1997, :person/self #db/id[:db.part/user -1000007], :db/id #db/id[:db.part/user -1000007]} ]
で、データ投入後のデータベースに対して、次のクエリーを投げます。
[ :find ?p ?self :where [?p :person/self ?self]]
:
ここで変数?p
に拘束されるのがエンティティ自信のid(long
)であり、?self
に拘束されるのが:preson/self
に格納された自分自身への参照(id)となります。そして、この二つは同じ値であることが想定されるので、取得した結果(HashSet
)に対して、次のようにassert
します。
results.each {PersistentVector vec -> assert vec[0] == vec[1] }
で、このテストはパスします。まあ、当然といえば当然です。というわけで、目論見のその1は完了です。
エンティティからアトリビュートをひっぺがして別のエンティティにしてしまう
で、やりたかったのがこれ。
先ほどのスキーマ定義を再掲します。
エンティティ | アトリビュート | 型 | カーディナリティ | 特記 |
---|---|---|---|---|
person | name | string | one | - |
person | sex | ref(enum) | one | :person.sex/woman, :person.sex/man |
person | birth | long | one | - |
person | self | ref(entity) | one | 自信への参照 |
ここで、次のような要望が発生します。
- わし、親にキラキラネームつけられて、つらい思いしてきたけぇ、名前変えたいんじゃ
まあ、datomicのデータベースは時間を持っているから、単純に変更してしまえばいいだけの話です。ただ、まあ、RDB脳が消え去っていないので、別のエンティティへ分割しようという結論に至ったことにしましょう。
ここで、スキーマ定義のトランザクションを見ると、ある一つのというか、当たり前の事実に気が付きます。それは、
スキーマ定義のトランザクションってただ単にデータを登録するトランザクションだよね
すると生じてくるのが、追加(定義)されたdb
エンティティのident
アトリビュートを更新するトランザクションを発行すれば、スキーマの定義をデータ非破壊で変更できることになるのではないかという予測です。というわけで、最初のスキーマ定義、データ投入のあとに、次のような:db/ident
を変えてしまうトランザクションを発生させてみたいと思います。
まず、:db/ident
を取得するクエリー
[ :find [?id ...] :in $ [?attr ...] :where [?id :db/ident ?attr]]
パラメーター
[:person/name :person/self]
スキーマ再定義
さて、取得したidに対して、次のように別のエンティティ/アトリビュートを再定義します。
:person/name
→:person.name/value
:person/self
→:person.name/person
今回、試しにやったとき、それぞれのエンティティ/アトリビュートの:db/ident
のidは次のとおりでした。
:person/name
→66
:person/self
→63
その結果、スキーマ変更をするトランザクションは次のようになります。
[ {:db/id 66, :db/ident :person.name/person} {:db/id 63, :db/ident :person.name/value} ]
このトランザクションをかけると、特に問題は発生しません。
ということはスキーマの変更が完了しているように思われる。
データが非破壊であることを確認
では、データは非破壊なのか、次のクエリーで確認します。
[ :find ?name ?sex ?birth :where [?p :person/sex ?s] [?s :db/ident ?sex] [?p :person/birth ?birth] [?n :person.name/value ?name] [?n :person.name/person ?p]]
これの結果を次のコードで表示させます。
results.each {PersistentVector vec -> log "name: [${vec[0]}], sex: [${vec[1]}], birth: [${vec[2]}]" }
実行すると、このようなログが出力されて、データ非破壊でスキーマ変更が成功したことがわかります。
name: [山本華子], sex: [:person.sex/woman], birth: [1997] name: [山田太郎], sex: [:person.sex/man], birth: [1995]
最初に投入したデータとまったく同じ状態で取得できていることがわかります。