今日はPreparedQuery
のようにクエリにパラメーターを渡すチュートリアルをやります。
なお、今日からは@Before
メソッド内でDatabase db = connection.db()
の準備をしておきます。
static final String URI = 'datomic:mem://seattle' static final String DIR = 'path/to/datomic-free/samples/seattle' private Connection connection private Database db @Rule public TestName test = new TestName() @Before void setup() { Peer.createDatabase(URI) connection = Peer.connect(URI) ['seattle-schema.edn', 'seattle-data0.edn'].each {file -> LOG.debug "==${file}==" List tx = DatomicUtil.from("${DIR}/${file}")[0] connection.transact(tx).get().each { LOG.debug("${it.key} - ${it.value}") } } db = connection.db() }
パラメーターを渡す
昨日の条件絞込の例ではクエリーを文字列で渡していましたが、毎回クエリーを組み立てるのも面倒です。また普通のDBと同じくdatomicも渡されたクエリー文字列をパースして結果をキャッシュに保持します。そのため、同じようなクエリーを発行するためにdatomicに毎回文字列を送るのも効率的とは言えません。
そこでdatomicでもパラメタライズドクエリーのような方式が提供されています。その方法はクエリーに:in
句を用います。
例えば、これまで使ってきた条件でデータを絞り込むクエリーは次のようなものでした。
[:find [?name ...] :where [?c :community/name ?name] [?c :community/type :community.type/twitter]]
この:community/type
の条件を:in
句を用いてパラメーターを与えられるようにします。
[:find [?name ...] :in $ ?type :where [?c :community/name ?name] [?c :community/type ?type]]
:in
句は:find
と:where
の間に記述する必要があります。:in
の最初にはデータソースを指定します。データソースは$
で始まるシンボルです(あまり意味がわかってない)。その次に、渡すパラメーターを拘束する変数を記述します。ここでは?type
を用いています。この?type
は:where
句の[?c :community/type ?type]
で参照します。
ところで前にも掲載した:community
のスキーマでは、:community/type
はReferenceであり、型としては:db/keyword
です。これを文字列でどのように扱うのでしょうか?答えは簡単で、ednのKeyword
形式にした文字列をそのまま渡せばよいです。例えば、:community.type/twitter
を指定したい場合は、文字列':community.type/twitter'
(これはGroovyの文字列形式)を引数に渡せば良いのです。あとはdatomicがよしなにやってくれます。
というわけで、能書きはここまでにして、動くコードを見てみましょう。
@Test void クエリにパラメタを渡す() { [ [type: ':community.type/twitter', expect: 6], [type: ':community.type/facebook-page', expect: 9] ].each { def results = Peer.query($/ [:find [?name ...] :in $ ?type :where [?c :community/name ?name] [?c :community/type ?type]]/$ as String, db, it.type) assert results.size() == it.expect LOG.debug "[${test.methodName}] ${results.join(', ')}" } }
実行結果
Magnolia Voice, Columbia Citizens, Discover SLU, Fremont Universe, Maple Leaf Life, MyWallingford Magnolia Voice, Columbia Citizens, Discover SLU, Fauntleroy Community Association, Eastlake Community Council, Fremont Universe, Maple Leaf Life, MyWallingford, Blogging Georgetown
チュートリアルドキュメントで結果の数がわかっていたので、assert
しています。
複数のパラメーターを渡す
上記の例ではパラメーターは一つでしたが、同じ型のパラメーターを複数渡すことも可能です。
その場合のクエリは次のような感じになります。
[:find [?name ...] :in $ [?type ...] :where [?c :community/name ?name] [?c :community/type ?type]]
これに対して、渡すパラメーターはList<Keyword>
でも渡すことができます。
では実行してみます。
@Test void 結果はdistinctされるため重複した名前を区別できない() { def keywords = ['twitter', 'facebook-page'].collect { "community.type/${it}" }.collect { keyword(it) } def results = Peer.query($/[ :find [?name ...] :in $ [?type ...] :where [?c :community/name ?name] [?c :community/type ?type]]/$ as String, db, keywords) assert results.size() < 15 LOG.debug "[${test.methodName}] results size[${results.size()}]" }
結果
results size[9]
テストメソッドで「結果がdistinctされる」とありますが、若干ここではまりました。
これの一つ前のクエリーのサンプルでは:community/type
が:community.type/twitter
の結果が6、:community.type/facebook-page
の結果が9だったので、単純に15になると予想したのですが、同じ:community/name
で異なる:community/type
を持つものがあったため、単純な合計にはなりませんでした。
また、上記ではList<Keyword>
を渡しましたが、次のようにdatomic.Util.read
でedn
のVector
にしてしまう方法もあります。
datomic.Util.read('[:community.type/twitter :community.type/facebook-page]')
複数の要素に対するパラメーターを渡す
ここまでは単一要素に対するパラメーターを渡してきましたが、複数の要素に対するパラメーターを渡すことも可能です。
そのクエリーは次の形になります。なお、条件はANDで結合されます。
[:find ?name ?type ?org :in $ [[?type ?org]] :where [?c :community/name ?name] [?c :community/type ?type] [?c :community/orgtype ?org]]
上記のクエリーではVector
のVector
をパラメーターで渡すことが可能です。
Vector
のVector
???
こんな感じ
[[:community.type/twitter :community.orgtype/community] [:community.type/website :community.orgtype/commercial]]
では、実際に動かしてみます。
@Test void 複数のパラメタを渡す() { // パラメーター // [ // [:community.type/email-list :community.orgtype/community] // [:cpmmunity.type/website :community.orgtype/commercial] // ] // を作成する def params = [ [type: 'email-list', orgtype: 'community'], [type: 'website', orgtype: 'commercial'] ].collect { it.collect { keyword("community.${it.key}/${it.value}") } } def results = Peer.query($/[ :find ?name ?type ?org :in $ [[?type ?org]] :where [?c :community/name ?name] [?c :community/type ?type] [?c :community/orgtype ?org]]/$ as String, db, params) assert results.size() == 15 // とりあえず、両方の条件を満たす結果を一つだけ表示するためのfilter def status = params.collect { new Keywords(type: it[0], org: it[1]) }.collectEntries { [it, true] } results.eachWithIndex {vec, idx -> def condition = new Keywords(type: vec[1], org: vec[2]) // filteringする if (status[condition]) { status[condition] = false LOG.debug "[${test.methodName}] name: [${vec[0]}], type: [${vec[1]}], org: [${vec[2]}]" } } } // 条件を保持するための簡易的なクラス @Canonical class Keywords { Keyword type Keyword org }
実行結果
name: [Leschi Community Council], type: [:community.type/email-list], org: [:community.orgtype/community] name: [InBallard], type: [:community.type/website], org: [:community.orgtype/commercial]
AND条件で絞りこまれた結果が取得できていますね。
ここらへんまで来ると、クエリーはだいたいどんな感じで書いていけばいいかがわかってきて、datomicやばい、楽しい٩(๑❛ᴗ❛๑)۶ってなってきます。
おわり
【2015/03/31 20:27 追記】
今朝ほどコメントいただきました。初心者ゆえに知らないメソッドなどのアドバイスを頂いており、大変感謝しています。
今回datomic.Util.list(Object...)
のアドバイスを頂きました。
ソースは公開されていないものの、IntelliJ IDEAのデコンパイルで内部的にどのような実装になっているかを確認した所、引数をそのままArrayList
にぶっこんで、Collections.unmodifiableList
でくるむというコードになっていました。
datomic.Peer.query
は文字列などをclojure.lang.Keyword
にうまく変換してくれて、いい感じにやってくれる優秀なメソッドなので、わざわざKeyword
を作らなくてもよい感はあります。一方で、groovyのList#collect
はリストのまま型変換していってくれる優秀なメソッドで、groovy5年生の僕はもはやgroovyのList#collect
が手に馴染んでいるという状態で、これを使っていました。
何が簡単であるかは使う人によって異なるので、これを読んでいる人(多分5人もいない)は好きな方法で使ってみるのがいいと思います。
あ、フォローしておくと、僕の全然知らないdatomicの話、毎回楽しみにさせて頂いています。これからもプラスアルファ情報を期待しております!