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


昨年、後輩から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はお好みで

```Gemfile
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にｒelationが貼られていることが確認できる。

![スクリーンショット 2017-01-09 20.03.31.png](https://qiita-image-store.s3.amazonaws.com/0/28543/5c0f5d41-9e57-118c-b7cd-52773bc28660.png "スクリーンショット 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:

## 参考サイト / 参考書籍
- [Neo4j.rb documentation](http://neo4jrb.readthedocs.io/en/8.0.x/)
- [Rubyからneo4jrb/neo4jを使ってneo4jにアクセスする](http://qiita.com/awwa/items/2d2c28f34f0796a30bce)
- [書籍：グラフ型データベース入門 - Neo4jを使う](https://www.amazon.co.jp/dp/B01EUK5RRQ/)
- 管理ページのチュートリアル