mike-neckのブログ

Java or Groovy or Swift or Golang

datomicチュートリアル2日目

datomicチュートリアル2日目。

今日はEntity APIとPull APIを試す。

Entity API

昨日もすでにさわっているAPIで、long型のentity idから想定されるentity mapに変換するAPIがEntity APIで、Database#entity(Object)から利用できる。

昨日と同じようなコードなのだが、再掲

    static final Logger LOG = LoggerFactory.getLogger(DatomicTutorial)
    static final String URI = 'datomic:mem://seattle'
    static final String DIR = 'path/to/datomic-free/samples/seattle'

    private Connection connection

    @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 = ReadTransaction.from("${DIR}/${file}")[0]
            connection.transact(tx).get().each {
                LOG.debug("${it.key} - ${it.value}")
            }
        }
    }

    @Test
    void queryWithEntityApi() {
        Database db = connection.db()
        HashSet<PersistentVector> results = Peer.query('[:find ?c :where [?c :community/name]]', db)
        int rows = 0
        results.each {PersistentVector vec ->
            if(rows ++ < 1) {
                long id = vec[0]
                EntityMap entity = db.entity(id)
                LOG.debug "${test.methodName} -> id[class: [${id.getClass()}], value: [${id}]], entity[class: [${entity.getClass()}], value: ${entity}]"
                String attr = entity.get(':community/name')
                LOG.debug ":community/name = ${attr}"
            }
        }
    }

実にgroovyらしくないコードになっていますが、おそらくクロージャコンパイラーの方で型安全性を保証しているからなのか、戻り値の型がObjectになっていたりして、IntelliJ IDEAでの補完が効かなかったので型情報を与えるためにJavaっぽいgroovyになっています。

この実行結果は次のとおり

queryWithEntityApi -> id[class: [class java.lang.Long], value: [17592186045440]], entity[class: [class datomic.query.EntityMap], value: {:db/id 17592186045440}]
:community/name = 15th Ave Community

Database#entity(Object)メソッドによって、entity idからentity mapへと変換(Long -> EntityMap)できることがわかる

EntityMap#toString()の結果が{:db/id 17592186045440}となっているのに、EntityMap#get(Object)の引数に:community/nameで値がとれてきたのが気持ち悪い感じがした。そこで、チュートリアルの説明を再確認すると、このcommunityというエンティティは次のような型になっている

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

というわけで、このEntityMapから他の値を取得できるかも確認してみる

    @Test
    void queryWithEntityApi() {
        Database db = connection.db()
        HashSet<PersistentVector> results = Peer.query('[:find ?c :where [?c :community/name]]', db)
        int rows = 0
        results.each {PersistentVector vec ->
            if(rows ++ < 1) {
                long id = vec[0]
                EntityMap entity = db.entity(id)
                LOG.debug "${test.methodName} -> id[class: [${id.getClass()}], value: [${id}]], entity[class: [${entity.getClass()}], value: ${entity}]"
                [':community/name', ':community/url'].each {
                    LOG.debug "$it = ${entity.get(it)}"
                }
            }
        }
    }

アトリビュート名.each以降の出力は次のとおり

:community/name = 15th Ave Community
:community/url = http://groups.yahoo.com/group/15thAve_Community/
:community/neighborhood = {:db/id 17592186045439}

とれた。

Pull API

ドキュメントによるとPull APIによって、ワイルドカードを用いた一つのクエリで全てのアトリビュートを取得することが可能になる。

先ほどのクエリーは[:find c? :where [c? :community/name]]であったが、Pull APIのクエリは[:find (pull c? [*]) :where[c? :community/name]]のようになる。

では、実際に動かしてみる。

    @Test
    void pullApi() {
        Database db = connection.db()
        def results = Peer.query('[:find (pull ?c [*]) :where [?c :community/name]]', db)
        LOG.debug "${test.methodName} -> ${results[0].get(0)}"
    }

実行結果は次のとおり

pullApi -> [:db/id:17592186045440, :community/name:15th Ave Community, :community/url:http://groups.yahoo.com/group/15thAve_Community/, :community/neighborhood:[:db/id:17592186045439], :community/category:[15th avenue residents], :community/orgtype:[:db/id:17592186045417], :community/type:[[:db/id:17592186045421]]]

先ほどの単純なクエリ[:find c? :where[c? :community/name]]の結果results[0].get(0)の型はjava.lang.Longであったが、今回のクエリ[:find (pull c? [*]) :where[?c :community/name]]の結果results[0].get(0)の型はjava.lang.Longではない。プリントデバッグをたくさんした結論を先に言うとそれはPersistentArrayMapである。したがって、下記に示すようなドキュメントにあるコードを実行すると、もれなくNullPointerExceptionが発生する。

    @Test
    void pullApiDocumented() {
        Database db = connection.db()
        def results = Peer.query('[:find (pull ?c [*]) :where[?c :community/name]]', db)
        def entity = db.entity(results[0].get(0))
        def nbh = entity.get(':community/neighborhood')
        LOG.debug "${nbh.get(':neighborhood/name')}"
    }
java.lang.NullPointerException: Cannot invoke method get() on null object

先ほども記述したとおり、Database#entity(Object)はentity id(Long)をentity map(EntityMap)に変換するメソッドである。したがって、引数にPersistentArrayMapを指定すると、entityが取得できずnullが返される。

というわけで、Tutorialをやっていたのに、Tutorialの道から外れ始めてしまったようだ…(´・ω・`)


まずはPersistentArrayMap(これは最終的にはMapインターフェースを実装している)のkeyとvalueの型を同定してみる。

    @Test
    void pullApiTypeCheck() {
        Database db = connection.db()
        def results = Peer.query('[:find (pull ?c [*]) :where[?c :community/name]]', db)
        Map map = results[0].get(0)
        map.each {
            LOG.debug "${test.methodName} -> key[class: ${it.key.getClass()}, value: ${String.format('%-24s', it.key)}], value[class: ${it.value.getClass()}, value: ${it.value}]"
        }
    }

結果は次のとおりになる

pullApiTypeCheck -> key[class: class clojure.lang.Keyword, value: :db/id                  ], value[class: class java.lang.Long, value: 17592186045440]
pullApiTypeCheck -> key[class: class clojure.lang.Keyword, value: :community/name         ], value[class: class java.lang.String, value: 15th Ave Community]
pullApiTypeCheck -> key[class: class clojure.lang.Keyword, value: :community/url          ], value[class: class java.lang.String, value: http://groups.yahoo.com/group/15thAve_Community/]
pullApiTypeCheck -> key[class: class clojure.lang.Keyword, value: :community/neighborhood ], value[class: class clojure.lang.PersistentArrayMap, value: [:db/id:17592186045439]]
pullApiTypeCheck -> key[class: class clojure.lang.Keyword, value: :community/category     ], value[class: class clojure.lang.PersistentVector, value: [15th avenue residents]]
pullApiTypeCheck -> key[class: class clojure.lang.Keyword, value: :community/orgtype      ], value[class: class clojure.lang.PersistentArrayMap, value: [:db/id:17592186045417]]
pullApiTypeCheck -> key[class: class clojure.lang.Keyword, value: :community/type         ], value[class: class clojure.lang.PersistentVector, value: [[:db/id:17592186045421]]]

Mapの型としてはMap<clojure.lang.Keyword, Object>といったところだろうか。

groovyでやっているはずなのに、なぜかクロージャーの世界に片足を突っ込んでた(;゙゚'ω゚'):

clojure.lang.Keywordのコード(decompileされたやつ)を見る限り、groovyからclojure.lang.Keywordインスタンスを生成する簡単な方法が見当たらない(なくもないけど、面倒い)。そこでgroovyを使い始めてもう5年にもなる僕は悪いことを考える。

    @BeforeClass
    static void extendMetaClass() {
        PersistentArrayMap.metaClass.invokeMethod = {String name, Object... args ->
            if (name == 'get' && args.length == 1 && args[0].class == String) {
                def arg = args[0]
                def newArg = delegate.find {it.key.toString() == arg}.key
                return delegate.get(newArg)
            }
            def metaMethod = PersistentArrayMap.metaClass.getMetaMethod(name, args)
            return metaMethod.invoke(delegate, args)
        }
    }

どうせPersistentArrayMapに対して呼び出すメソッドgetであるので、getメソッドinvokeされた時に、引数の数が1で、その型がStringである場合は、PersistentArrayMapkeySet()で名前が一致するkeyを見つけて、それを元にgetメソッドを再invokeするようにする。

そうして、やっとチュートリアルのようなコードを書くことができた

    @Test
    void queryWithPullApiAndRelation() {
        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++ < 1) {
                PersistentArrayMap map = vec.get(0)
                PersistentArrayMap nbh = map.get(':community/neighborhood')
                LOG.debug "${test.methodName} -> nbh[class: ${nbh.getClass()}, value: ${nbh}]"
                EntityMap neighborhood = db.entity(nbh.get(':db/id'))
                String name = neighborhood.get(":neighborhood/name")
                LOG.debug "${test.methodName} -> neighborhood[class: [${neighborhood.getClass()}], value: [${neighborhood}]], name[class: [${name.getClass()}], value: [${name}]]"
            }
        }
    }

実行結果は次のとおり

queryWithPullApiAndRelation -> nbh[class: class clojure.lang.PersistentArrayMap, value: [:db/id:17592186045439]]
queryWithPullApiAndRelation -> neighborhood[class: [class datomic.query.EntityMap], value: [{:neighborhood/name "Capitol Hill", :db/id 17592186045439}]], name[class: [class java.lang.String], value: [Capitol Hill]]

ちょっとチュートリアルにかかれているコードが実行できないと、なかなか先に進めないのが辛いところです。

新しもの好きで2012年(日本ではじめてdatomicについて言及された記事が出た)頃から触っている人には、「プークスクス、datomicをclojureで書かなくていいのは小学生までだよね〜」と言われてしまいそうだけど、Thought WorksのTech Radarにも登場したくらいだから、今後日本でも注目される可能性を考慮すれば、この辺の面倒な辺りを乗り越えていく手段を提示しておくのは無駄なことではないと思う。

リレーション

上記の例では:communityから:neighborhoodを取得していましたが、datomicにおいてはリレーションは双方向でリレーションを取得できるとのことです。そのサンプルコードを試す前にプリントデバッグで疲れたので、続きは明日。


以上、Tutorialすらまともに出来ない弱小おじさんのチュートリアル2日目でした。


【2015/03/28 0:16 追記】

コメントいただきました。datomic.Util.read(':keyword')Keyword取得できるそうです。

というわけで、テストコード書いてみました。

    @Test
    void gettingKeyWord() {
        [
                [arg: 'symbol', expectedType: Symbol, asString: 'symbol'],
                [arg: ':keyword', expectedType: Keyword, asString: ':keyword']
        ].each {
            def obj = Util.read(it.arg)
            assert obj.class == it.expectedType
            assert obj.toString() == it.asString
        }
    }

見事テストが通りました。感謝です!