Cypher発行SQL関数
雨が降ってバイクで遊びに行けない(´・ω・`)
まあ、いい機会だからPostgreSQL 9.3 beta1のJSON型演算子を検証してみるのもいいか。
PostgreSQL 9.3で強化されたJSON型
9.2の時点では格納(の背景でのパース)と取り出し程度しか機能がなく、ビミョーな感じだったJSON型だけど、9.3では演算子や各種関数がかなり強化されたようだ。
PostgreSQL: Documentation: 9.3: JSON Functions and Operators
昨日、9.3betaをインストールしていろいろ試す環境はできたので、JSON型の操作等をいろいろ試してみようか。
まずは基本的なキーからの値の取得。
json=# SELECT '{"a":1,"b":2}'::json->'b'; ?column? ---------- 2 (1 row) json=#
なるほど、こんな感じなのか。
Cypher発行SQL関数
さて、本格的に検証する前に、色んなパターンのJSONデータを作成する環境もつくっておきたい。
そして手元にはグラフデータベースNeo4jがあるじゃなイカ!
Neo4jにはREST APIがあり、さらにNeo4jの問合わせ言語CypherもREST APIで投げることができる。
そのレスポンスもJSONなので、それをそのまま返却するSQL関数を作成すれば、Neo4j上の結果をPostgreSQLのJSON演算子で扱うことができるはず。
幸い、先日作成したneo4j_fdw(neo4j_fdw プロトタイプ - 日々の記録 別館参照)の内部関数を呼び出せば、数行程度でこのSQL関数は実装できる。
ということで、さくっと実装してデータベースに組み込んでみた。
json=# \dx List of installed extensions Name | Version | Schema | Description -----------+---------+------------+------------------------------------------- neo4j_fdw | 1.0 | public | foreign-data wrapper for flat file access plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language (2 rows) json=# \dx+ neo4j_fdw Objects in extension "neo4j_fdw" Object Description ------------------------------------------ foreign-data wrapper neo4j_fdw function exec_cypher(text,text) function neo4j_fdw_handler() function neo4j_fdw_validator(text[],oid) (4 rows) json=#
exec_cypherがその関数だ。
さっそく動かしてみる。
json=# SELECT exec_cypher('http://localhost:7474/db/data/cypher', '{"query" : "START n=node(*) WHERE n.gender? = \"Famale\" RETURN n.name as name, n.gender? as gender"}'); exec_cypher ----------------------------------------------------------------------------------------------------------------------------------------------------------------- { + "columns" : [ "name", "gender" ], + "data" : [ [ "伊勢", null ], [ "日向", null ], [ "赤城", "Famale" ], [ "加賀", "Famale" ], [ "信濃", "Famale" ], [ "飛龍", "Famale" ], [ "蒼龍", "Famale" ] ]+ } (1 row) json=#
お、一応JSONらしきものが返ってきた。
ちょっと凝ったCypherクエリを発行してみる。
これは両思い(お互いにフォローしているユーザ)を検索するものだ。
json=# SELECT exec_cypher('http://localhost:7474/db/data/cypher', '{"query":"START n=node(*) MATCH p=fm<-[:follow]-n<-[:follow]-fm RETURN n.name as my_name, n.gender as my_gender, fm.name as follower_name, fm.gender as follower_gender" }'); exec_cypher ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------- { + "columns" : [ "my_name", "my_gender", "follower_name", "follower_gender" ], + "data" : [ [ "大和", "Male", "武蔵", "Male" ], [ "大和", "Male", "信濃", "Famale" ], [ "大和", "Male", "長門", "Male" ], [ "武蔵", "Male", "信濃", "Famale" ], [ "武蔵", "Male", "大和", "Male" ], [ "長門", "Male", "陸奥", "Male" ], [ "長門", "Male", "大和", "Male" ], [ "陸奥", "Male", "長門", "Male" ], [ "赤城", "Famale", "加賀", "Famale" ], [ "加賀", "Famale", "赤城", "Famale" ], [ "信濃", "Famale", "武蔵", "Male" ], [ "信濃", "Famale", "大和", "Male" ] ]+ } (1 row) json=#
例えば、このJSON結果から"->"演算子を使ってdataをキーとする結果を抜き出してみる。
json=# SELECT exec_cypher('http://localhost:7474/db/data/cypher', '{"query":"START n=node(*) MATCH p=fm<-[:follow]-n<-[:follow]-fm RETURN n.name as my_name, n.gender as my_gender, fm.name as follower_name, fm.gender as follower_gender" }')->'data'; ?column? ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ -------------------------------------------------------------------------------- [ [ "大和", "Male", "武蔵", "Male" ], [ "大和", "Male", "信濃", "Famale" ], [ "大和", "Male", "長門", "Male" ], [ "武蔵", "Male", "信濃", "Famale" ], [ "武蔵", "Male", "大和", "Male" ], [ "長門", "Male", "陸奥", "Male" ], [ "長門", "Male", "大和", "Male" ], [ "陸奥", "Male", "長門", "Male" ], [ "赤城", "Famale", "加賀", "Famale" ], [ "加賀", "Famale", "赤城", "Famale" ], [ "信濃", "Famale", "武蔵", "Male" ], [ "信濃", "Famale", "大和", "Male" ] ] (1 row) json=#
この結果から、さらに2番めのエントリを抜き出す(0相対なので1を指定する)。
json=# SELECT exec_cypher('http://localhost:7474/db/data/cypher', '{"query":"START n=node(*) MATCH p=fm<-[:follow]-n<-[:follow]-fm RETURN n.name as my_name, n.gender as my_gender, fm.name as follower_name, fm.gender as follower_gender" }')->'data'->1; ?column? -------------------------------------- [ "大和", "Male", "信濃", "Famale" ] (1 row) json=#
ちなみに、CypherのRETURN句でノード全体を返却すると、以下の様な結構複雑なJSONが返却される。
json=# SELECT exec_cypher('http://localhost:7474/db/data/cypher', '{"query":"START n=node(*) RETURN n" }'); exec_cypher ------------------------------------------------------------------------------------------------------------------ { + "columns" : [ "n" ], + "data" : [ [ { + "paged_traverse" : "http://localhost:7474/db/data/node/1/paged/traverse/{returnType}{?pageSize,leaseTime}", + "outgoing_relationships" : "http://localhost:7474/db/data/node/1/relationships/out", + "data" : { + "location" : "奈良県", + "description" : "でかーい。俺の46サンチ砲が火を吹くぜ", + "name" : "大和", + "gender" : "Male" + }, + "traverse" : "http://localhost:7474/db/data/node/1/traverse/{returnType}", + "all_typed_relationships" : "http://localhost:7474/db/data/node/1/relationships/all/{-list|&|types}", + "all_relationships" : "http://localhost:7474/db/data/node/1/relationships/all", + "property" : "http://localhost:7474/db/data/node/1/properties/{key}", + "self" : "http://localhost:7474/db/data/node/1", + "outgoing_typed_relationships" : "http://localhost:7474/db/data/node/1/relationships/out/{-list|&|types}", + "properties" : "http://localhost:7474/db/data/node/1/properties", + "incoming_relationships" : "http://localhost:7474/db/data/node/1/relationships/in", + "incoming_typed_relationships" : "http://localhost:7474/db/data/node/1/relationships/in/{-list|&|types}", + "extensions" : { + }, + "create_relationship" : "http://localhost:7474/db/data/node/1/relationships" + } ], [ { + "paged_traverse" : "http://localhost:7474/db/data/node/2/paged/traverse/{returnType}{?pageSize,leaseTime}", + "outgoing_relationships" : "http://localhost:7474/db/data/node/2/relationships/out", + "data" : { + "location" : "東京都", + "description" : "影が薄いです", + "name" : "武蔵", + "gender" : "Male" + }, + "traverse" : "http://localhost:7474/db/data/node/2/traverse/{returnType}", + "all_typed_relationships" : "http://localhost:7474/db/data/node/2/relationships/all/{-list|&|types}", + "all_relationships" : "http://localhost:7474/db/data/node/2/relationships/all", + "property" : "http://localhost:7474/db/data/node/2/properties/{key}", + "self" : "http://localhost:7474/db/data/node/2", + "outgoing_typed_relationships" : "http://localhost:7474/db/data/node/2/relationships/out/{-list|&|types}", + "properties" : "http://localhost:7474/db/data/node/2/properties", + "incoming_relationships" : "http://localhost:7474/db/data/node/2/relationships/in", + "incoming_typed_relationships" : "http://localhost:7474/db/data/node/2/relationships/in/{-list|&|types}", + "extensions" : { + }, + "create_relationship" : "http://localhost:7474/db/data/node/2/relationships" + } ], [ { + (以下略)