mike-neckのブログ

Java or Groovy or Swift or Golang

datomicチュートリアル8日目 #datomic

今日はデータの追加をやります。

トランザクションのデータストラクチャー

トランザクションをあらわすデータストラクチャーは次のような形になります。

[
[data-structure-list]
[data-structure-list]
...
]

あるいは

[
{data-structure-map}
{data-structure-map}
...
]

個々のデータ操作を行うリストかマップを要素に持つリストです。これはデータの追加、更新、削除においてすべて共通する構造ですので、確実に覚えておいたほうが良いです。

データ追加のdata structure

データを追加する際のdata structureは次の二つがあります。

リスト形式の場合:

[:db/add new-entity-id attribute-name attribute-value]

リスト形式の場合、複数の属性に値を設定することはできません。属性1つずつ丹念に心をこめて:db/addしていきます。

マップ形式の場合:

{
    :db/id new-entity-id
    attribute-name attribute-value
    attribute-name attribute-value
    attribute-name attribute-value
    ...
}

マップ形式の場合の式は、内部的にはリスト形式に変換されて実行されます。丹念に心をこめて書いていた:db/addは機械的に実行されるようになります。

一時ID

先に示したリスト/マップの追加用のdata structureでnew-entity-idのところに、既存のエンティティのIDを指定すると、データの更新になります。したがって、新しいデータを追加する場合は一時IDを利用しないといけません。

一時IDを取得する式は次のとおりになります。

#db/id[partition-name long-value]

上記の式でlong-valueはオプションのため、省略が可能です。しかし、他の新しいエンティティへの参照を登録する必要がある場合には、そのエンティティを参照できるようにするために、long-valueを指定しておいたほうがよいでしょう。

partition-nameはデータが所属するパーティションの名前を指定します。結合を頻繁に行うエンティティ同士は同じパーティションに所属している方がデータアクセスの効率がよくなります。デフォルトでいくつかのパーティションが提供されていますが、当面は:db.part/userというパーティションを使います。

リスト形式でのデータ追加

チュートリアルのデータ構造を利用して、サンプルのデータを登録します。

登録するデータは次のようなデータです。

エンティティ:community

属性名 内容
:community/name "Hoge"
:community/url "http://localhost:8000"
:community/category ["foo", "bar", "baz"]
:community/type :community.type/twitter
:community/orgtype :community.orgtype/community
:community/neighborhood 下記を参照

エンティティ:neighborhood

属性名 内容
:neighborhood/name "Bar"
:neighborhood/district 下記を参照

エンティティ:district

属性名 内容
:district/name "Foo"
:district/region :region/ne

上記の内容を追加するトランザクションデータは次のようになります。

datomic-tutorial8-add-as-list.edn

[
[:db/add #db/id[:db.part/user -1]
    :district/name "Foo"]
[:db/add #db/id[:db.part/user -1]
    :district/region :region/ne]
[:db/add #db/id[:db.part/user -2]
    :neighborhood/name "Bar"]
[:db/add #db/id[:db.part/user -2]
    :neighborhood/district #db/id[:db.part/user -1]]
[:db/add #db/id[:db.part/user -3]
    :community/name "Hoge"]
[:db/add #db/id[:db.part/user -3]
    :community/url "http://localhost:8000"]
[:db/add #db/id[:db.part/user -3]
    :community/category "foo"]
[:db/add #db/id[:db.part/user -3]
    :community/category "bar"]
[:db/add #db/id[:db.part/user -3]
    :community/category "baz"]
[:db/add #db/id[:db.part/user -3]
    :community/orgtype :community.orgtype/community]
[:db/add #db/id[:db.part/user -3]
    :community/type :community.type/twitter]
[:db/add #db/id[:db.part/user -3]
    :community/neighborhood #db/id[:db.part/user -2]]
]

これを流し込むコードは次のようになります。

ClassLoader loader = getClass().classLoader
def countQuery = '[:find ?c :where [?c :community/name]]'

@Test
void 新規データをtransactionDataStructureのdbAddで追加() {
    def url = loader.getResource('datomic-tutorial8-add-as-list.edn')
    assert url != null
    def tx = DatomicUtil.from(url)
    def dbAfter = connection.transact(tx[0]).get().get(Connection.DB_AFTER)
    def before = Peer.query(countQuery, db).size()
    def after = Peer.query(countQuery, dbAfter).size()
    assert after == before + 1
}

追加前のDatabaseであるdbと追加後のDatabsedbAfterに対して[:find ?c [?c :community/name]]を実行すると、dbAfterの方がレコード数が1件多いことが想定されますので、このようなassertになっています。

マップ形式でのデータ追加

リスト形式で追加したのと同じようなデータをマップ形式で追加するトランザクションデータは次のようになります。

datomic-tutorial8-add-as-map.edn

[
{:db/id #db/id[db.part/user -1],
    :district/name "Foo",
    :district/region :region/ne}
{:db/id #db/id[db.part/user -2],
    :neighborhood/name "Bar",
    :neighborhood/district #db/id[db.part/user -1]}
{:db/id #db/id[db.part/user -3],
    :community/name "Hoge",
    :community/url "http://localhost:8000",
    :community/category ["foo" "bar" "baz"],
    :community/orgtype :community.orgtype/community,
    :community/type :community.type/twitter,
    :community/neighborhood #db/id[:db.part/user -2]}
]

実際にこれを流し込むコードは先ほどのコードと変わりません。読み込むリソースが異なるだけです。

リスト形式のdata structureをAPI経由で追加する

上記のサンプルではedn形式でデータを追加する方法を書きましたが、DatomicではJava APIが提供されています。

@Test
void 新規データをdbAddを用いてAPI経由で追加する() {
    def tempId = {long id ->
        Peer.tempid(':db.part/user', id)
    }
    def dbAdd = ':db/add'
    def list = [
            //districtのデータ
            [dbAdd, tempId(-1), ':district/name', 'Foo'],
            [dbAdd, tempId(-1), ':district/region', ':region/ne'],
            //neighborhoodのデータ
            [dbAdd, tempId(-2), ':neighborhood/name', 'Bar'],
            [dbAdd, tempId(-2), ':neighborhood/district', tempId(-1)],
            //communityのデータ
            [dbAdd, tempId(-3), ':community/name', 'hoge'],
            [dbAdd, tempId(-3), ':community/url', 'http://localhost:8000'],
            [dbAdd, tempId(-3), ':community/category', '["foo", "bar", "baz"]'],
            [dbAdd, tempId(-3), ':community/orgtype', ':community.orgtype/community'],
            [dbAdd, tempId(-3), ':community/type', ':community.type/twitter'],
            [dbAdd, tempId(-3), ':community/neighborhood', tempId(-2)]
    ]
    def dbAfter = connection.transact(list).get().get(Connection.DB_AFTER)
    def before = Peer.query(countQuery, db).size()
    def after = Peer.query(countQuery, dbAfter).size()
    assert after == before + 1
}

単純にデータ操作をするリストのリストを作って、Connection#transact(List)に渡すだけです。一時IDはPeer#tempid(String, long)あるいはPeer#tempid(String)により作成できます。

マップ形式のdata structureをAPI経由で追加する

リストでできることはマップでもできます。

@Test
void 新規データをAPIから追加する() {
    // テンポラリーIDを発生させるパーティション
    def tempId = {long id ->
        Peer.tempid(':db.part/user', id)
    }
    //追加データ
    def lists = [
            [//districtのデータ
                    ':db/id': tempId(-1),
                    ':district/name': 'Foo',
                    ':district/region': ':region/ne'],
            [//neighborhoodのデータ
                    ':db/id': tempId(-2),
                    ':neighborhood/name': 'Bar',
                    ':neighborhood/district': tempId(-1)],
            [//communityのデータ
                    ':db/id': tempId(-3),
                    ':community/name': 'Hoge',
                    ':community/url': 'http://localhost:8000',
                    ':community/category': ['foo', 'bar', 'baz'],
                    ':community/orgtype': ':community.orgtype/community',
                    ':community/type': ':community.type/twitter',
                    ':community/neighborhood': tempId(-2)]
    ]
    def dbAfter = connection.transact(list).get().get(Connection.DB_AFTER)
    def before = Peer.query(countQuery, db).size()
    def after = Peer.query(countQuery, dbAfter).size()
    assert after == before + 1
}

リスト形式では:db/addから始まるリストでしたが、マップ形式では':db/id': Peer.tempid(':db.part/user')を含むマップでデータを追加できます。


データ操作のチュートリアルが微妙にわかりづらかったので、データの追加を調べるだけで1日に費やせる時間を使ってしまいました(´・ω・`)

次回はデータの更新・削除をやりたいと思います。