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
である場合は、PersistentArrayMap
のkeySet()
で名前が一致する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 } }
見事テストが通りました。感謝です!