mike-neckのブログ

Java or Groovy or Swift or Golang

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

今日は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です。これを文字列でどのように扱うのでしょうか?答えは簡単で、ednKeyword形式にした文字列をそのまま渡せばよいです。例えば、: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.readednVectorにしてしまう方法もあります。

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]]

上記のクエリーではVectorVectorをパラメーターで渡すことが可能です。

VectorVector???

こんな感じ

[[: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の話、毎回楽しみにさせて頂いています。これからもプラスアルファ情報を期待しております!