前回の復習
前回のコメントから、Keyword
がUtil.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]
指定したとおりのデータが取得できていますね。
土日で時間があったため、結構長めのエントリーになりました。
次回はパラメーターを指定するクエリーをやりたいと思います。
おわり