今日はデータの削除をやります。
データの削除に用いるトランザクションのデータ構造
データ削除に用いるトランザクションのデータ構造は次の二通りがあります。
属性の値を削除する方法
[ [:db/retract entity-id attribute-name value] ... ]
:db/retract
で始まるリストで指定したエンティティの指定した属性の指定した値を削除します。指定した値が現在の値と異なる場合は何も起こりません。
エンティティを削除する方法
[ [:db.fn/retractEntity entity-id] ]
:db.fn/retractEntity
は組み込みの関数で、指定したエンティティのすべての属性を削除します。
これらは、前回、前々回で紹介したリスト(:db/add
)形式/マップ(:db/id
)形式に対応しているような感じです。
指定した値を削除するとは?
では、指定した値を削除するというのがどういう意味なのか、確認してみましょう。なお、今日のテスト用のデータも昨日と同じデータを用います。
まず、エンティティIDを1件取得するクエリーを発行します。これは次のようなクエリーで取得できます。
[ :find ?c . :where [?c :community/name "Hoge"]]
これは、:community/name
が"Hoge"の:community
のエンティティIDを1件だけ取得します。結果が複数あっても任意の1件のエンティティIDが返されます。このクエリーを実行する場合は条件を必ず一意になるように指定するように気をつける必要があります。というのも、狙い通りのエンティティに対して更新・削除の操作ができないからです。その後、:db/retract
を用いてエンティティに設定してある値を削除します。
private List<String> attributes = ['name', 'url', 'category', 'orgtype', 'neighborhood', 'type'] private void log(String s) { LOG.debug "[${test.methodName}] ${s}" } static String FIND_HOGE_ID = $/[ :find ?c . :where [?c :community/name "Hoge"]]/$ @Test void retractSample() { // :community/nameがHogeのエンティティIDを取得する long id = Peer.query(FIND_HOGE_ID, db) // 上記のエンティティの:community/orgtypeの値:community.orgtype/commercialを削除します def transaction = "[[:db/retract ${id} :community/orgtype :community.orgtype/commercial]]" def tx = DatomicUtil.from(new StringReader(transaction))[0] Database afterDb = connection.transact(tx).get().get(Connection.DB_AFTER) // 削除後に同一条件でエンティティIDを取得します long afterId = Peer.query(FIND_HOGE_ID, afterDb) // エンティティIDに変更がないことを検証します assert id == afterId logData('before', db, id) logData('after', afterDb, afterId) } private void logData(String when, Database database, long id) { def entity = database.entity(id) def values = attributes.toKeywords().collect { def value = entity.get(it) "${it}: [${value}]" }.join(', ') log "$when id: [$id], ${values}" }
テストデータで:community/name
が"Hoge"のデータは1件だけですので、最初のクエリーはレコードを一意に取得してきます。テストデータの:community/orgtype
はすべて:community.orgtype/commercial
に設定されていますので、:db/retract
はこの値が設定されたという事実を削除します。しかし、エンティティそのものが削除されるわけではないので、変更後のデータベースから同じ条件でレコードを取得すると、同じIDが返されることが想定されます。そこで、エンティティIDが同一であることを検証しています。最後の方にあるlogData
というメソッドは変更前のエンティティと変更後のエンティティを表示するメソッドです。
これを実行してみましょう。
datomic.db - {:tid 1, :pid 51039, :phase :begin, :event :db/add-fulltext} datomic.db - {:tid 1, :pid 51039, :phase :end, :msec 0.0455, :event :db/add-fulltext} before id: [17592186045656], :community/name: [Hoge], :community/url: [http://localhost:8000/hoge], :community/category: [[kudoh, test]], :community/orgtype: [:community.orgtype/commercial], :community/neighborhood: [{:db/id 17592186045655}], :community/type: [:community.type/facebook-page] after id: [17592186045656], :community/name: [Hoge], :community/url: [http://localhost:8000/hoge], :community/category: [[kudoh, test]], :community/orgtype: [null], :community/neighborhood: [{:db/id 17592186045655}], :community/type: [:community.type/facebook-page]
この結果を表で表すと次のようになります。
属性 | 変更前 | 変更後 |
---|---|---|
:community/name |
Hoge | Hoge |
:community/url |
http://localhost:8000/hoge |
http://localhost:8000/hoge |
:community/category |
kudoh、test | kudoh、test |
:community/orgtype |
:community.orgtype/commercial |
null |
:community/neighborhood |
17592186045655 | 17592186045655 |
:community/type |
:community.type/facebook-page |
:community.type/facebook-page |
予定通り:community/orgtype
の値が:community.orgtype/commercial
からnull
に変わりました。そして、他の値は変更されていません。このような形で:db/retract
は指定外の属性の値はそのまま残しつつ、指定した属性の指定した値を削除します。
では、指定した値が間違えていた場合はどうなるのでしょうか?これも試してみましょう。
@Test void retractAntiSample() { // :community/nameがHogeのエンティティIDを取得する long id = Peer.query(FIND_HOGE_ID, db) // 上記のエンティティの:community/orgtypeの値:community.orgtype/commercialを削除します def transaction = "[[:db/retract ${id} :community/orgtype :community.orgtype/community]]" def tx = DatomicUtil.from(new StringReader(transaction))[0] Database afterDb = connection.transact(tx).get().get(Connection.DB_AFTER) // 削除後に同一条件でエンティティIDを取得します long afterId = Peer.query(FIND_HOGE_ID, afterDb) // エンティティIDに変更がないことを検証します assert id == afterId // :community/orgtypeに変更がないことを検証します assert db.entity(id).get(':community/orgtype') == afterDb.entity(id).get(':community/orgtype') log "before ${db.entity(id).get(':community/orgtype')}" log "after ${afterDb.entity(id).get(':community/orgtype')}" }
実行するとテストはパスして、次のように変更されていないことが確認できます。
before :community.orgtype/commercial after :community.orgtype/commercial
:db/retract
でエンティティ1件を削除する
:db/retract
でエンティティ1件を削除する場合は、次のようなdata structureを実行します。
- エンティティのすべての要素に対して
:db/retract
を実行する - 属性のカーディナリティがManyの場合は、すべての値について
:db/retract
を実行する - 属性が他のエンティティへの参照の場合は、参照先のエンティティIDをvalueに指定する
上記の要件を満たしつつ、:community/name
が"Hoge"であるエンティティを削除するプログラムは次のようになります。
@Test void dbRetractでエンティティを削除する() { //:community/nameがHogeのエンティティIDを取得する def id = Peer.query(FIND_HOGE_ID, db) def entity = db.entity(id) //トランザクション作成 def transactions = attributes.toKeywords().collect {attr -> //値の型によって指定する値が異なる def value = entity.get(attr) def klass = value.getClass() //カーディナリティがManyの場合はリストにして後でflatten()する klass.equals(PersistentHashSet) ? value.collect {[attribute: attr, value: it]}: //参照型の場合は、:db/idを指定する klass.equals(EntityMap) ? [attribute: attr, value: value.get(':db/id')]: //その他の場合はそのまま使う [attribute: attr, value: value] }.flatten().collect { //トランザクションのデータ構造に変換 def dq = it.value.getClass().equals(Long)? '' : '"' "[:db/retract ${id} ${it.attribute} $dq${it.value}$dq]" }.join('\n') //リストのリストを作る def transaction = $/[ $transactions ]/$ log transaction //トランザクション実行 def tx = DatomicUtil.from(new StringReader(transaction)) def afterDb = connection.transact(tx[0]).get().get(Connection.DB_AFTER) //:community/nameがHogeのエンティティIDは存在しない def afterId = Peer.query(FIND_HOGE_ID, afterDb) assert afterId == null //元のIDからエンティティは取り出せる def e = afterDb.entity(id) assert e != null //ただし要素は何もない assert e.keySet().size() == 0 }
一度取得してきたidから各属性の値を取得した後、:db/retract
のリストを作成します。作成されたトランザクションdata structureでトランザクションを発生させて、変更後のデータベースから同一条件でデータを検索します。この時、条件に一致するエンティティは存在しないので、null
が返ってきます。そして、次が面白いところですが、最初に取得したIDからエンティティ(EntityMap
)への変換を行うことはできます。ただし、属性が一つもないのでkeySet()
のサイズは0件です。
途中でattributes.toKeywords()
というのを呼び出していますが、これは次のように@Before
の際にGroovyのメタクラスを用いて拡張したメソッドで、属性のキーワードに変更するメソッドです。
attributes.metaClass.define { toKeywords = { return delegate.collect { ":community/${it}" as String } } }
これを実行すると、実際のトランザクションdata structureが表示されます。
[ [:db/retract 17592186045656 :community/name "Hoge"] [:db/retract 17592186045656 :community/url "http://localhost:8000/hoge"] [:db/retract 17592186045656 :community/category "kudoh"] [:db/retract 17592186045656 :community/category "test"] [:db/retract 17592186045656 :community/orgtype ":community.orgtype/commercial"] [:db/retract 17592186045656 :community/neighborhood 17592186045655] [:db/retract 17592186045656 :community/type ":community.type/facebook-page"] ]
このように、大量のトランザクションdata structureを作成しないといけないため、非常に面倒いです。そこで、Datomicには
エンティティを削除する:db.fn/retractEntity
という組み込み関数
が用意されています。
これは先ほど書いたようにエンティティIDを指定すれば、そのエンティティが削除されます。
@Test void dbFnRetractでエンティティを削除する() { // :community/nameがHogeのエンティティIDを取得する def id = Peer.query(FIND_HOGE_ID, db) // データ取消 def transaction = "[[:db.fn/retractEntity ${id}]]" log transaction def tx = DatomicUtil.from(new StringReader(transaction)) def afterDb = connection.transact(tx[0]).get().get(Connection.DB_AFTER) //:community/nameがHogeのエンティティIDは存在しない def afterId = Peer.query(FIND_HOGE_ID, afterDb) assert afterId == null //元のIDからエンティティは取り出せる def e = afterDb.entity(id) assert e != null //ただし要素は何もない assert e.keySet().size() == 0 }
先ほどのコードに比べて、かなり短くなっていることがわかります。そして、得られる結果は同一です。
API経由で
ここまでは文字列を組み立てていましたが、リストでもできます。
:db/retract
の場合
def tx = attributes.toKeywords().collect {attr -> //値の型によって指定する値が異なる def value = entity.get(attr) def klass = value.getClass() //カーディナリティがManyの場合はリストにして後でflatten()する klass.equals(PersistentHashSet) ? value.collect {[attribute: attr, value: it]}: //参照型の場合は、:db/idを指定する klass.equals(EntityMap) ? [attribute: attr, value: value.get(':db/id')]: //その他の場合はそのまま使う [attribute: attr, value: value] }.flatten().collect { [':db/retract', id, it.attribute, it.value] } def afterDb = connection.transact(tx).get().get(Connection.DB_AFTER)
:db.fn/retractEntity
の場合
def tx = [[':db.fn/retractEntity', id]] def afterDb = connection.transact(tx).get().get(Connection.DB_AFTER)
以上。次回はカーディナリティがManyのアトリビュートの更新についてです。