mike-neckのブログ

Java or Groovy or Swift or Golang

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

今日はデータの更新をやります。


データ更新のためのdata structure

Datomicではデータの更新用のクエリーというかdata-structureというのはありません。

前回紹介したデータを追加するときのdata-structureをそのまま利用します。

具体的に書くと次のようになります。

リスト形式

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

あるいは

マップ形式

[
{
    :db/id entity-id,
    attribute-name value,
    attribute-name value
    ...
}
]

データ追加の場合との違い

データを追加する場合は、上記のentity-idの部分に一時ID(#db/id[:db.part/user long-value])を用いていました。

データを更新する場合は、上記のentity-idに実際のIDを指定して実行します。

テストデータ

データ更新を試すために、今回は次に示すレコードを追加します。

:community/name :community/orgtype :community/category
Foo :community.orgtype/commercial test
Hoge :community.orgtype/commercial test, kudoh
Bar :community.orgtype/commercial test, drink, alcohol

今回追加したレコードは:community/orgtype:community.orgtype/commercialであり、:community/categoryの中にtestが必ず含まれているので、次のクエリーで検索をすると、追加したレコード3件だけが必ず取得できます。

[
:find
    ?name
    ?c
:where
    [?c :community/name ?name]
    [?c :community/orgtype :community.orgtype/commercial]
    [?c :community/category "test"]]

なお、具体的に追加したデータはgistにおいてあります。

リスト(:db/add)形式でデータを更新する

まずは:db/addのdata-structure形式でデータを更新してみます。

static String QUERY = $/[
:find
    ?name
    ?c
:where
    [?c :community/name ?name]
    [?c :community/orgtype :community.orgtype/commercial]
    [?c :community/category "test"]]/$

static String COUNT = $/[
:find
    [?name ...]
:where
    [?c :community/name ?name]]/$

@Test
void データをdata_structureのdb_addで更新する() {
    //@Beforeで追加したテストデータを取得する
    def results = Peer.query(QUERY, db)
    //@Beforeで追加したテストデータは3件
    assert results.size() == 3
    results.each {
        LOG.debug "[${test.methodName}] name: [${it[0]}], id: [${it[1]}]"
    }
    //:community/nameがHogeのもののidを取得する
    def id = results.findAll { it[0] == 'Hoge'}.collect { it[1] }[0]
    //:community/nameがHogeのものの:community/orgtypeを:community.orgtype/communityに変更する
    def dataStructure = "[[:db/add ${id} :community/orgtype :community.orgtype/community]]"
    LOG.debug "[${test.methodName}] data-structure -> ${dataStructure}"
    def tx = DatomicUtil.from(new StringReader(dataStructure))
    def dbAfter = connection.transact(tx[0]).get().get(Connection.DB_AFTER)
    //最初と同じ条件でデータを取得する
    def afterResults = Peer.query(QUERY, dbAfter)
    //件数は2件になっている
    assert afterResults.size() == 2
    afterResults.each {
        LOG.debug "[${test.methodName}] name: [${it[0]}], id: [${it[1]}]"
    }
    //更新前と更新後で:communityのレコード数が変わってないことを検証
    def before = Peer.query(COUNT, db)
    def after = Peer.query(COUNT, db)
    assert before.size() == after.size()
}

これを実行すると次のようなログが表示されます。

[データをdata_structureのdb_addで更新する] name: [Foo], id: [17592186045653]
[データをdata_structureのdb_addで更新する] name: [Bar], id: [17592186045659]
[データをdata_structureのdb_addで更新する] name: [Hoge], id: [17592186045656]
[データをdata_structureのdb_addで更新する] data-structure -> [[:db/add 17592186045656 :community/orgtype :community.orgtype/community]]
datomic.db - {:tid 1, :pid 49009, :phase :begin, :event :db/add-fulltext}
datomic.db - {:tid 1, :pid 49009, :phase :end, :msec 0.0956, :event :db/add-fulltext}
[データをdata_structureのdb_addで更新する] name: [Foo], id: [17592186045653]
[データをdata_structureのdb_addで更新する] name: [Bar], id: [17592186045659]

クエリーの検索条件は:community/orgtype:community.orgtype/commercialで、:community/category"hoge"が含まれているものです。データ更新前では該当するレコードが3件あるのは前述のとおりです。:community/name"Hoge"のエンティティIDを取得してから、次のような:community/orgtype:community.orgtype/communityに更新するdata structureを作成します。

[[:db/add 17592186045656 :community/orgtype :community.orgtype/community]]

このdata structureを実行した後のデータベースに対して最初に実行したクエリーを実行します。そしてレコードが2件になっていることが検証されます。最後に更新前後のデータベースから:communityエンティティの全レコード件数を取得して比較します。これが変わってないことから、:community/name"Hoge"のレコードが削除されたわけではないことが確認できます。

マップ(:db/id)の形式でレコードを更新する

上記と同じ操作を:db/id形式のdata structureでも実行してみます。

@Test
void データをdata_structureのdb_idで更新する() {
    //@Beforeで追加したテストデータを取得する
    def results = Peer.query(QUERY, db)
    //@Beforeで追加したテストデータは3件
    assert results.size() == 3
    results.each {
        LOG.debug "[${test.methodName}] name: [${it[0]}], id: [${it[1]}]"
    }
    //:community/nameがHogeのもののidを取得する
    def id = results.findAll { it[0] == 'Hoge'}.collect { it[1] }[0]
    //:community/nameがHogeのものの:community/orgtypeを:community.orgtype/communityに変更する
    def dataStructure = "[{:db/id ${id}, :community/orgtype :community.orgtype/community}]"
    LOG.debug "[${test.methodName}] data-structure -> ${dataStructure}"
    def tx = DatomicUtil.from(new StringReader(dataStructure))
    def dbAfter = connection.transact(tx[0]).get().get(Connection.DB_AFTER)
    //最初と同じ条件でデータを取得する
    def afterResults = Peer.query(QUERY, dbAfter)
    //件数は2件になっている
    assert afterResults.size() == 2
    afterResults.each {
        LOG.debug "[${test.methodName}] name: [${it[0]}], id: [${it[1]}]"
    }
    //更新前と更新後で:communityのレコード数が変わってないことを検証
    def before = Peer.query(COUNT, db)
    def after = Peer.query(COUNT, db)
    assert before.size() == after.size()
}

これを実行すると次のようなログが表示されます。

name: [Foo], id: [17592186045653]
name: [Bar], id: [17592186045659]
name: [Hoge], id: [17592186045656]
data-structure -> [{:db/id 17592186045656, :community/orgtype :community.orgtype/community}]
datomic.db - {:tid 1, :pid 49164, :phase :begin, :event :db/add-fulltext}
datomic.db - {:tid 1, :pid 49164, :phase :end, :msec 0.249, :event :db/add-fulltext}
name: [Foo], id: [17592186045653]
name: [Bar], id: [17592186045659]

前回でも書きましたが、:db/idのようなマップ形式のdata structureは内部的には:db/addに変換されて実行されます。したがって、次のdata structureは先ほど実行したものと同等のものです。

[{:db/id 17592186045656, :community/orgtype :community.orgtype/community}]

APIから実行する

APIから実行する場合も、前回とあまり変わりません。一時IDPeer.tempid(String, long)を使っていた場所に実際のID(long)を指定するだけです。

リスト(:db/add)形式でデータ更新
@Test
void データをAPIのdb_addで更新する() {
    //@Beforeで追加したテストデータを取得する
    def results = Peer.query(QUERY, db)
    //@Beforeで追加したテストデータは3件
    assert results.size() == 3
    results.each {
        LOG.debug "[${test.methodName}] name: [${it[0]}], id: [${it[1]}]"
    }
    //:community/nameがHogeのもののidを取得する
    def id = results.findAll { it[0] == 'Hoge'}.collect { it[1] }[0]
    //:community/nameがHogeのものの:community/orgtypeを:community.orgtype/communityに変更する
    def tx = [':db/add', id, ':community/orgtype', ':community.orgtype/community']
    //データ更新
    def dbAfter = connection.transact([tx]).get().get(Connection.DB_AFTER)
    //最初と同じ条件でデータを取得する
    def afterResults = Peer.query(QUERY, dbAfter)
    //件数は2件になっている
    assert afterResults.size() == 2
    afterResults.each {
        LOG.debug "[${test.methodName}] name: [${it[0]}], id: [${it[1]}]"
    }
    //更新前と更新後で:communityのレコード数が変わってないことを検証
    def before = Peer.query(COUNT, db)
    def after = Peer.query(COUNT, db)
    assert before.size() == after.size()
}
マップ(:db/id)形式でデータ更新
@Test
void データをAPIのdb_idで更新する() {
    //@Beforeで追加したテストデータを取得する
    def results = Peer.query(QUERY, db)
    //@Beforeで追加したテストデータは3件
    assert results.size() == 3
    results.each {
        LOG.debug "[${test.methodName}] name: [${it[0]}], id: [${it[1]}]"
    }
    //:community/nameがHogeのもののidを取得する
    def id = results.findAll { it[0] == 'Hoge'}.collect { it[1] }[0]
    //:community/nameがHogeのものの:community/orgtypeを:community.orgtype/communityに変更する
    def tx = [':db/id': id,
              ':community/orgtype': ':community.orgtype/community']
    //データ更新
    def dbAfter = connection.transact([tx]).get().get(Connection.DB_AFTER)
    //最初と同じ条件でデータを取得する
    def afterResults = Peer.query(QUERY, dbAfter)
    //件数は2件になっている
    assert afterResults.size() == 2
    afterResults.each {
        LOG.debug "[${test.methodName}] name: [${it[0]}], id: [${it[1]}]"
    }
    //更新前と更新後で:communityのレコード数が変わってないことを検証
    def before = Peer.query(COUNT, db)
    def after = Peer.query(COUNT, db)
    assert before.size() == after.size()
}

まとめ

以上、データの追加と更新はつぎのようにまとめられます。

  • データ追加と更新は同じdata structureで実行できる
  • データ追加と更新方法にはリスト(:db/add)形式とマップ(:db/id)形式の二通りの方法がある
  • マップ(:db/id)形式では内部的にリスト(:db/add)に変換されてから実行される
  • データ追加時は一時IDを指定する
  • データ更新時は実際のIDを指定する