LoginSignup
11
10

More than 5 years have passed since last update.

RailsでActiveNodeやActiveRelを使ってNeo4jにアクセスしてみる

Last updated at Posted at 2017-01-09

昨年、後輩から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を実行する必要がある。
また、以下の設定も別途必要になる。

config/application.rb
require 'neo4j/railtie'
config/neo4j.yml
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のまま)だったが、どんなペナルティがあるかは不明。

app/models/person.rb
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を指定してます。

app/models/person_in.rb
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が貼られていることが確認できる。

スクリーンショット 2017-01-09 20.03.31.png

(2つだけだと相当地味だな... :last_quarter_moon_with_face:

ActiveNode & ActiveRel

ActiveNodeインスタンスよりActiveRelを経由して別ノードを取得してみる。
そのためにhas_manyで関連を定義しておく。

app/models/person.rb
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情報を定義しないと動かない部分にはだいぶ苦しめられた。
まぁ、ちゃんとドキュメントには書いてあったので、私の目が節穴だっただけですが。。。:sweat:

参考サイト / 参考書籍

11
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
10