昨年、後輩からRDBおじさんとディスられた勢いで触ったまま放置していたneo4j。
色々と中途半端な状態だったので、2017年になって改めて触ってみたので、それについて書いてみました。
なお、Model定義してRailsっぽい感じでやる事を主眼にやったので、内容は超入門です。
環境
ruby 2.4.0
Rails 5.0.1
sqlite 3
neo4j 3.1.0
※ Ruby2.4を利用するとFixnumやBignumのwarningが出るので、気になる場合はRuby2.3系で試した方がいいかも(2017年1月7日時点)
事前準備
neo4jはhomebrewで入れる。
gemインストール後にrakeで入れることも出来るみたいだが、バージョンが古い(2.2.x)みたいなので。
➜ ~ brew install neo4j
➜ ~ neo4j start
# http://localhost:7474にアクセスすると、初回はパスワード変更を求められるので下記のように設定
# user: neo4j
# pass: hogehoge
Gemfileに追加して、neo4jをインストールする。
※ RSpecはお好みで
gem 'neo4j'
# RSpecを利用する場合
group :development, :test do
gem 'neo4j-rspec'
end
neo4j-rspec
を利用する場合は、spec/spec_helper.rbに以下の1行を追加。
config.include Neo4j::RSpec::Matchers
Cypherで接続してみる
neo4j版SQLとも言えるCypherという専用クエリで接続してみる。
➜ app_neo4j_sample git:(master) ✗ bin/rails c
# neo4jに接続する"http://[ユーザ名]:[パスワード]@[URL]"形式
[1] pry(main)> neo4j_session = Neo4j::Session.open(:server_db, 'http://neo4j:hogehoge@localhost:7474')
# Cypherでnodeを作成する
=> Neo4j::Server::CypherSession url: 'http://localhost:7474/db/data/' version: '3.1.0'
[2] pry(main)> neo4j_session.query("CREATE (Keanu:Person {name:'Keanu Reeves', born:1964})")
=> #<Enumerator: ...>
# 同じくCypherを叩いて上記で作成したnodeを取得する
[3] pry(main)> person = neo4j_session.query("MATCH (result:Person) RETURN result").first.result
=> CypherNode 0 (70272397802360)
[4] pry(main)> person[:name]
=> "Keanu Reeves"
Modelから操作する
Modelから操作するには目的に応じて以下の2つのModuleのいずれかをincludeしたModelクラスを生成する必要がある。
- ActiveNode ... ノードを扱う
- ActiveRel ... リレーションシップを扱う
扱うデータ・モデルSchema情報をneo4jに登録するためにmigrationを実行する必要がある。
また、以下の設定も別途必要になる。
require 'neo4j/railtie'
development:
type: http
url: http://neo4j:hogehoge@localhost:7474
ActiveNode
generateで必要ファイルを作成し、neo4j:migrate
を実行する。
➜ app_neo4j_sample git:(master) ✗ bin/rails g neo4j:model person
create app/models/person.rb
create db/neo4j/migrate/20170108130757_person.rb
invoke rspec
create spec/models/person_spec.rb
➜ app_neo4j_sample git:(master) ✗ bin/rake neo4j:migrate
== 20170108130757 Person: running... ===========================================
CYPHER CREATE CONSTRAINT ON (n:`Person`) ASSERT n.`uuid` IS UNIQUE
== 20170108130757 Person: migrated (0.1327s) ===================================
Modelにname, born属性をpropertyで定義しておく。
migrateファイルには反映しなくても大丈夫(defaultのまま)だったが、どんなペナルティがあるかは不明。
class Person
include Neo4j::ActiveNode
property :name
property :born, type: Integer, default: 0
end
consoleを立上げ、Modelからneo4jを操作してみる。
# Modelからnodeを作成する
[1] pry(main)> person = Person.new(name: 'Hugo Weaving', born: 1960)
=> #<Person uuid: nil, born: 1960, name: "Hugo Weaving">
[2] pry(main)> person.save
HTTP REQUEST: 8ms GET http://localhost:7474/db/data/schema/constraint (0 bytes)
HTTP REQUEST: 13ms GET http://localhost:7474/db/data/schema/index (0 bytes)
CYPHER CREATE (n:`Person`) SET n = {props} RETURN n | {:props=>{:uuid=>"003fda64-8bfc-49a5-b72e-2d082f94fd8c", :name=>"Hugo Weaving", :born=>1960}}
HTTP REQUEST: 12ms POST http://localhost:7474/db/data/transaction (1 bytes)
HTTP REQUEST: 11ms POST http://localhost:7474/db/data/transaction/6/commit (0 bytes)
=> true
# 作成したnodeを取得する
[3] pry(main)> person2 = Person.find_by(name: 'Hugo Weaving')
HTTP REQUEST: 10ms GET http://localhost:7474/db/data/schema/constraint (0 bytes)
HTTP REQUEST: 2ms GET http://localhost:7474/db/data/schema/index (0 bytes)
Person
MATCH (n:`Person`)
WHERE (n.name = {n_name})
RETURN n
LIMIT {limit_1} | {:n_name=>"Hugo Weaving", :limit_1=>1}
HTTP REQUEST: 44ms POST http://localhost:7474/db/data/transaction/commit (1 bytes)
=> #<Person uuid: "003fda64-8bfc-49a5-b72e-2d082f94fd8c", born: 1960, name: "Hugo Weaving">
[4] pry(main)> person2.name
=> "Hugo Weaving"
ActiveRel
modelクラスを作成し、migrateを実行する。
➜ app_neo4j_sample git:(master) bin/rails g neo4j:model person_in
create app/models/person_in.rb
create db/neo4j/migrate/20170109073046_person_in.rb
error rspec [not found]
➜ app_neo4j_sample git:(master) ✗ bin/rake neo4j:migrate
== 20170109073046 PersonIn: running... =========================================
CYPHER CREATE CONSTRAINT ON (n:`PersonIn`) ASSERT n.`uuid` IS UNIQUE
== 20170109073046 PersonIn: migrated (0.4957s) =================================
ActiveRelクラスを定義する。
リレーションはperson同士で貼るので、from, to 共にpersonを指定してます。
class PersonIn
include Neo4j::ActiveRel
from_class :Person
to_class :Person
end
これまでに作成した2つのpersonを関連づかせてみる。
[1] pry(main)> from_person = Person.find_by(name: 'Hugo Weaving')
[2] pry(main)> to_person = Person.find_by(name: 'Keanu Reeves')
[3] pry(main)> rel = PersonIn.new(from_node: from_person, to_node: to_person)
=> #<PersonIn>
[4] pry(main)> rel.save
CYPHER
MATCH
(from_node),
(to_node)
WHERE
(ID(from_node) = {from_node_id}) AND
(ID(to_node) = {to_node_id})
CREATE (from_node)-[rel:`PERSON_IN` {rel_create_props}]->(to_node)
RETURN rel | {:from_node_id=>2, :to_node_id=>0, :rel_create_props=>{}}
HTTP REQUEST: 82ms POST http://localhost:7474/db/data/transaction (1 bytes)
HTTP REQUEST: 6ms POST http://localhost:7474/db/data/transaction/11/commit (0 bytes)
=> true
管理ツールで確認すると、2つのpersonにrelationが貼られていることが確認できる。
(2つだけだと相当地味だな...
ActiveNode & ActiveRel
ActiveNodeインスタンスよりActiveRelを経由して別ノードを取得してみる。
そのためにhas_many
で関連を定義しておく。
has_many :out, :persons, model_class: :Person, rel_class: :PersonIn
あとは普通のActiveRecordと同様に取得することが出来る。
[1] pry(main)> person = Person.find_by(name: 'Hugo Weaving')
[2] pry(main)> person.persons
Person#persons
MATCH (person2)
WHERE (ID(person2) = {ID_person2})
MATCH (person2)-[rel1:`PERSON_IN`]->(result_persons:`Person`)
RETURN result_persons | {:ID_person2=>2}
HTTP REQUEST: 38ms POST http://localhost:7474/db/data/transaction/commit (1 bytes)
=> #<AssociationProxy Person#persons [#<Person uuid: nil, born: 1964, name: "Keanu Reeves">]>
[3] pry(main)> person.persons.first.name
=> "Keanu Reeves"
まとめ
基本的なところまで何とか動かす事が出来たので、次はもうちょっと応用的な事をやってみようかな。
これだけだとグラフDB使う意味が皆無だし。
とりあえずmigrateしてSchema情報を定義しないと動かない部分にはだいぶ苦しめられた。
まぁ、ちゃんとドキュメントには書いてあったので、私の目が節穴だっただけですが。。。