mike-neckのブログ

Java or Groovy or Swift or Golang

datomic スキーマ変更をデータ非破壊でおこなう #datomic

ここ最近書いていた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/name66
  • :person/self63

その結果、スキーマ変更をするトランザクションは次のようになります。

[
  {: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]

最初に投入したデータとまったく同じ状態で取得できていることがわかります。


RDBに比べて、カラム(アトリビュート)を別のテーブル(エンティティ)に移動させることが簡単な感じがします。