mike-neckのブログ

Java or Groovy or Swift or Golang

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

前回の復習

前回のコメントから、KeywordUtil.read(String)で取得できることがわかったので、最後のサンプルコードを修正してみます。

@Test
void queryPullApiWithRelation() {
    Database db = connection.db()
    def results = Peer.query('[:find (pull ?c [*]) :where[?c :community/name]]', db)
    int rows = 0
    results.each {PersistentVector vec ->
        if (rows ++ < 2) {
            // 結果列の1項目目(エンティティそのもの)を取得
            PersistentArrayMap map = vec.get(0)
            // エンティティから:community/neighborhoodを取得
            PersistentArrayMap nbh = map.get(keyword(':community/neighborhood'))
            // :community/neighborhoodから:db/idを取得してエンティティに変換
            EntityMap neighborhood = db.entity(nbh.get(keyword(':db/id')))
            // :neighborhoodエンティティから:neighborhood/nameを取得
            def name = neighborhood.get(':neighborhood/name')
            LOG.debug "[${test.methodName}] neighborhood: [class: [${name.getClass()}], value: ${name}]"
        }
    }
}

実行結果は次のとおりになる

neighborhood: [class: [class java.lang.String], value: Capitol Hill]
neighborhood: [class: [class java.lang.String], value: Admiral (West Seattle)]

見事にクエリーから関連する項目を取ってくることができました

ただ、ご覧のとおり面倒な操作である事実は否めません。

アトリビュートの値を取得する

ここまでのクエリーは次のような形になっています。

[:find ?c :where[?c :community/name]]

ただ、アプリケーションはデータの変換をやりたいのではなく、データそのものを取得したいのです。datomicでは:findに変数を追加して:whereで値を拘束する次のようなクエリーが提供されています。

[:find ?c ?n :where[?c :community/name ?n]]

では、この式を実行してみましょう。

@Test
void queryWithAttributesValue() {
    Database db = connection.db()
    def results = Peer.query('[:find ?c ?n :where[?c :community/name ?n]]', db)
    def vec = results[0]
    assert vec.size() == 2
    [[index: 0, expectedType: Long], [index: 1, expectedType: String]].each {
        assert vec[it.index].getClass() == it.expectedType
        def e = db.entity(vec[it.index])
        LOG.debug "[${test.methodName}] [idx[$it.index]: [class: [${vec[it.index].getClass()}], value: [${vec[it.index]}], entity: [$e]]"
    }
}

このクエリーから返ってくる結果が?cはエンティティのidであり?n:community/nameの値であることが想定されます。では実行してみましょう。

[idx[0]: [class: [class java.lang.Long], value: [17592186045461], entity: [{:db/id 17592186045461}]]
[idx[1]: [class: [class java.lang.String], value: [Ballard Avenue], entity: [null]]

みごとassertに失敗することなく通りました。また、実際に:community/nameの値も取得できているようです。

Longのidなんかいらないので、文字列だけくれ

ところで毎回、クエリーの結果にエンティティのidが入ってくるのも面倒な話だし、結果はStringのリストの形でほしいわけです。そこで、datomicでは次のようなクエリーを書くことができます。

[:find [?n ...] :where[_ :community/name ?n]]

  • :findの部分には[?n ...]で結果をリストで取得するように指示します。
  • :whereでは_という見慣れない記号が出ていますが、いちいちエンティティを何らかの変数に拘束する必要がないときに用いることができます。そして、最終的に:community/nameを変数?nに拘束します。Scalaなどでもよく用いていますね(棒読み

では、実行してみましょう。

@Test
void queryAttributeList() {
    Database db = connection.db()
    def results = Peer.query('[:find [?n ...] :where[_ :community/name ?n]]', db)
    assert results.size() == 132
    results.eachWithIndex {value, index ->
        assert value.class == String
        if(index < 2) {
            LOG.debug "[${test.methodName}] :community/name[${value}]"
        }
    }
}

実行結果

:community/name[KOMO Communities - Ballard]
:community/name[Ballard Blog]

文字列のリストで取得できたようです。

一つのエンティティの中の複数の要素を取得する

DBにアクセスするなら、値を1つずつではなく、がっつり取ってきたいです。datomicでももちろんできます。SQLより若干クエリーが面倒なことになっていますが、それほど難しいものではないです。

[:find ?n ?u :where [?c :community/name ?n][?c :community/url ?u]]

  • :find ?n ?u複数の値を取得します
  • :whereに渡す式の最初では?c:communityエンティティを拘束して、なおかつ:community/name?nに拘束します
  • :whereに渡す式の2つ目では拘束された?cのエンティティから:community/urlを取り出して?uに拘束します

これにより、一つのエンティティから複数の要素を取得できます。

@Test
void queryMultipleAttributes() {
    Database db = connection.db()
    def results = Peer.query('[:find ?n ?u :where [?c :community/name ?n] [?c :community/url ?u]]', db)
    assert results.size() == 150

    def vec = results[0]
    assert vec.size() == 2

    [0, 1].each {
        assert vec[it].class == String
    }

    LOG.debug "[${test.methodName}] :community/name[${vec[0]}], :community/url[${vec[1]}]"
}

実行結果は次のとおり

:community/name[Broadview Community Council], :community/url[http://groups.google.com/group/broadview-community-council]

シンボル

ところで、クエリーで?uとか?nとか使ってますけど、これは?nameとか?-urlとかurlとか使えるのでしょうか?実際に試してみます。

@Test
void クエリの変数で使える形式() {
    Database db = connection.db()
    def queryBase = [
            [index: 0, variable: '?name', attr: ':community/name'],
            [index: 1, variable: '?-url', attr: ':community/url'],
            [index: 2, variable: 'cat', attr: ':community/category']
    ]
    [2,3].each {attrs ->
        def qb = queryBase.findAll {it.index < attrs}
        def vars = qb.collect{it.variable}.join(' ')
        def condition = qb.collect {"[?c ${it.attr} ${it.variable}]"}.join('')
        def query = "[:find ${vars} :where ${condition}]" as String
        LOG.debug(query)
        def results = Peer.query(query, db)
        assert results.size() == 150
    }
}

実行結果

[:find ?name ?-url :where [?c :community/name ?name][?c :community/url ?-url]]
[:find ?name ?-url cat :where [?c :community/name ?name][?c :community/url ?-url][?c :community/category cat]]
java.lang.IllegalArgumentException: Argument cat in :find is not a variable

例外が投げられました。曰く「catは変数ではない」と。一方で?-urlは変数として使うことができるようです。

実際の所、クエリーに関するドキュメントを見ると、変数の定義は次のようになっています。

variable = symbol starting with "?"

(変数は?で始まるシンボル)

また、シンボルとはedn(extensible data notation)での用語で、次のように定義されています。

Symbols begin with a non-numeric character and can contain alphanumeric characters and . * + ! - _ ? $ % & = < >. If -, + or . are the first character, the second character (if any) must be non-numeric. Additionally, : # are allowed as constituent characters in symbols other than as the first character.

簡単にまとめると次のとおり

  • 最初の文字に使えない文字は数字、:#
  • -+.で始まる場合は次の文字は数字以外の文字

もうちょっと定義があるのですが、それは上記のednのドキュメントを読むほうが早いです。

カーディナリティがManyのやつ

「文字列だけくれ」という見出しの部分の:community/nameだけを取り出すクエリーは結果の数が132でしたが、:communityを取ってくると結果が150あります。この違いは何でしょうか?ここで、前回も掲載したチュートリアルに書かれているエンティティの定義を再掲します。

attribute type cardinality
:community/name String One
:community/url String One
:community/neighborhood Reference One
:community/category String Many
:community/orgtype Reference One
:community/type Reference One

:community/categoryのカーディナリティがmanyになっています。これが重複している分です。

そこで

条件をつけて絞り込むクエリー

[:find ?community ?category :where[?community :community/name "belltown"][?community :community/category ?category]]

を発行してみます。

@Test
void カーディナリティがManyなやつ() {
    Database db = connection.db()
    def results = Peer.query('[:find ?community ?category :where[?community :community/name "belltown"][?community :community/category ?category]]', db)
    assert results.size() == 2
    results.each {vec ->
        LOG.debug "[${test.methodName}] category[${vec[1]}]"
    }
}

実行結果

category[news]
category[events]

データ絞込

条件でのデータの絞り込みは次のような形で行います。

  • :whereに続けるvectorで絞込、エンティティから取得する要素の選択、関連するエンティティの指定を行う
  • [?entity :entity/attribute ?attribute]の形で?entityに拘束されているエンティティ/要素から指定した:entity/attribute?attributeに拘束する
  • [?entity :entity/attribute "value"]の形で?entityに拘束されているエンティティ/要素の指定した:entity/attributeに対して指定した"value"でマッチングする

例:

[:find
    ?name ?url
:where
    [?c :community/name "Broadview Community Council"]
    [?c :community/url ?url]
    [?c :community/name ?name]]
  • ?c:community/nameのエンティティに拘束
  • :community/name"Broadview Community Council"でマッチング
  • ?cに拘束されたエンティティ(:community)の要素:community/url?urlに拘束
  • ?cに拘束されたエンティティ(:community)の要素:community/name?nameに拘束
  • 結果として?name?urlを返す

では、実際に試してみましょう。

@Test
void queryWithCondition() {
    Database db = connection.db()
    def results = Peer.query($/
[:find
    ?name ?url
:where
    [?c :community/name "Broadview Community Council"]
    [?c :community/url ?url]
    [?c :community/name ?name]]/$ as String, db)
    results.each {vec ->
        LOG.debug "[${test.methodName}] find community with name[Broadview Community Council] -> name[${vec[0]}], url[${vec[1]}]"
    }
}

実行結果

find community with name[Broadview Community Council] -> name[Broadview Community Council], url[http://groups.google.com/group/broadview-community-council]
find community with name[Broadview Community Council] -> name[Broadview Community Council], url[http://www.broadviewseattle.org/]

指定した:community/nameを持ったエンティティを取得できていることがわかります。

複数のエンティティにまたがった条件での絞込

:whereに指定するVectorの記述の仕方によって、エンティティの結合も可能です。

:where
    [?c :community/neighborhood ?n]
    [?n :neighborhood/district ?d]
    [?d :district/region :region/ne]
  • ?cのエンティティ/要素から:community/neighborhood?nに拘束
  • ?nに拘束されたエンティティ:neighborhoodから:neighborhood/district?dに拘束
  • ?dに拘束されたエンティティ:districtから:district/region:region/neでマッチング

このように複数のエンティティを結合しつつ、条件での絞り込みも可能になります。

@Test
void queryAcrossReferences() {
    Database db = connection.db()
    List results = Peer.query($/
[:find
    [?c_name ...]
:where
    [?c :community/name ?c_name]
    [?c :community/neighborhood ?n]
    [?n :neighborhood/district ?d]
    [?d :district/region :region/ne]]/$ as String, db)
    assert results.size() == 9
    results.eachWithIndex{name, index ->
        if (index < 2 || index == 8) {
            LOG.debug "[${test.methodName}] :community/name[${name}]"
        }
    }
}

実行結果は次のとおりになります。

:community/name[Maple Leaf Community Council]
:community/name[Hawthorne Hills Community Website]
:community/name[Magnuson Environmental Stewardship Alliance]

絞込されたデータの数は9件で最初と2番目、そして最後の要素を表示しているので3件だけ表示されています。このように複数のエンティティにまたがったクエリーの実行も可能です。

これを応用すれば、複数のエンティティで条件を指定しつつ、複数のエンティティから要素の値を取得することも可能です。

@Test
void queryAcrossReferencesRetrievingCommunityNameAndRegionName() {
    Database db = connection.db()
    def results = Peer.query($/
[:find
    ?cname
    ?nname
    ?rname
:where
    [?c :community/name "Broadview Community Council"]
    [?c :community/name ?cname]
    [?c :community/neighborhood ?n]
    [?n :neighborhood/name ?nname]
    [?n :neighborhood/district ?d]
    [?d :district/region ?r]
    [?r :db/ident ?rname]]/$ as String, db)
    results.each {PersistentVector vec ->
        LOG.debug "[${test.methodName}] communityName: [${vec[0]}], neighborhoodName: [${vec[1]}], regionName: [${vec[2]}]"
    }
}

このサンプルコードでは:community/name"Broadview Community Council"でマッチングしつつ、:community/name:neighborhood/name:district/regionの値を取得してきます。なお、:district/regionの値についてはenum型なので:db/identというエンティティ/要素の値を?rnameに拘束しています。実行結果は次のとおりになります。

communityName: [Broadview Community Council], neighborhoodName: [Broadview], regionName: [:region/sw]

指定したとおりのデータが取得できていますね。


土日で時間があったため、結構長めのエントリーになりました。

次回はパラメーターを指定するクエリーをやりたいと思います。


おわり