Neo4j アプリケーションを作るには?

Neo4j アプリケーションを作るには?


はじめまして
キャリアDiv. マーケティング企画統括部 データアナリティクス部 マーケティングテクノロジーグループのSEIDAといいます。

部門のKPI集計、可視化をなどを生業をしています。
アーキ選定〜実装〜運用まで、担当してますので、日々エンジニアとして楽しくやっております。
さて、今回はここ最近私の中でHotなNeo4jについて書きます。

Neo4j

Neo4jは、NeoTechnology社が開発している「グラフ指向データベース」です。
Neo4jやグラフ指向データベースが何かは、書籍や多くのブログ記事がありますので、今回は述べません。
また、「Cypher」の話もしません。

今回の話題

CypherやWebインターフェースの記事は見かけますが、実際にアプリケーションを作りたいときに、どうすればいいかと悩む事があるかと思います。
また情報も少ないので、その点について書きます。

Neo4jのAPI

Neo4jのアプリケーションを作成するのに2つの方法があります。

  • REST APIを使用する
  • 組み込み(Java)APIを使用する

アプリケーション構成は、こんな感じになります。

image1

基本はREST API

REST API使用したアプリケーションが標準の構成だと考えています。

Neo4jを操作するためのライブラリや、フレームワークがありますが、ほぼREST APIをターゲットにしています。
Neo4jサーバが、かなりしっかりしてますので、余程の理由がなければ、組み込みAPIを使ってスクラッチで組む必要は無いと考えています。

SDN(Spring Data Neo4j)というフレームワークがありますが、最新版の4系から、リモート(REST API)のNeo4jを前提に作り直されました。
http://docs.spring.io/spring-data/data-neo4j/docs/4.0.0.RELEASE/reference/html/#_about_spring_data_neo4j_4
NeoTechnology社の人もコミッターにいますので、この流れが主流になると考えています。

どうしても、Neo4jサーバが嫌な方は「org.neo4j.server.WrappingNeoServerBootstrapper」を使う手もありますが、
Deprecatedですので、いつ無くってもおかしくない状態です。

REST APIだとパフォーマンスが問題になる場合がありますが、ServerPluginやUnmanagedExtesionsで対応するようです。
http://neo4j.com/docs/stable/server-extending.html
※ServerPluginやUnmanagedExtesionsについては、次回予定

組み込みAPIの使い道は、ServerPluginやUnmanagedExtensionsにて使用するケースと
Neo4jサーバを利用しないバッチ処理が考えられます。

REST APIをつかう

では、Neo4jサーバのREST APIと通信するライブラリなどを使ってみます。
以下からは、Macでの実行手順ですので、お使いの環境に適時読み替えてください。

neo4j-jdbcを使う

neo4j-contributeで、REST APIと通信するJDBCが公開されてますでの、まずはこちらを使ってみます。
https://github.com/neo4j-contrib

jarの作成

git clone git@github.com:neo4j-contrib/neo4j-jdbc.git
cd neo4j-jdbc/
mvn package
ls target/neo4j-jdbc-2.2-SNAPSHOT-jar-with-dependencies.jar

neo4j 起動

ダウンロードページからNeo4jを、ダウンロードします。
http://neo4j.com/download/other-releases/

tar xzvf neo4j-community-2.3.1-unix.tar.gz
neo4j-community-2.3.1/bin/neo4j console

初回起動の場合、WebインターフェースでBasic認証のパスワード変更しますが、
不要な場合は、conf/neo4j-server.properties dbms.security.auth_enabledをfalseに変更してください。

Neo4j-jdbcを使ったサンプルコード

Webインターフェースのチュートリアルサンプルで用意されている「Movie」のデータを投入したうえで試してください。
また、作成したneo4j-jdbcはクラスパスが通った場所に配置してください。

import java.sql.*;
import java.util.Map;
 
public class NeoJdbcSample {
 
    public static void main(String... args) throws SQLException {
 
        Connection con = DriverManager.getConnection("jdbc:neo4j://localhost:7474/","neo4j","neo");
 
        /*
         データファイル指定でも可能
         依存関係 'org.neo4j:neo4j:2.2.7'
         Neo4j2.3から組み込みAPIのIFが変更になっているのでエラーとなる。
         2.3系は今のところ(2015/11/30時点)は動かない. PRはでているみたい。
         */        
        //Connection con = DriverManager.getConnection("jdbc:neo4j:file:/Users/k_seida/neo4j-community-2.2.7/data/graph.db");
 
        try (Statement stmt = con.createStatement()) {
            ResultSet rs = stmt.executeQuery("MATCH (n) return count(n) as count");
 
            while (rs.next()) {
                System.out.println(rs.getString("count"));
            }
 
            rs = stmt.executeQuery("MATCH (tom {name: \"Tom Hanks\"}) RETURN tom");
            int born = 0;
            while (rs.next()) {
                Map n = rs.getObject("tom", Map.class);
                System.out.print("name: " + n.get("name"));
                System.out.println("  born: " + n.get("born"));
 
                born = (int)n.get("born");
            }
 
            // READMEにある「Neo4j-2.0.1-SNAPSHOT Version」では、「?」は利用できない
            String query = "MATCH (p:Person) where p.born = ? return p";
            PreparedStatement preStmt = con.prepareStatement(query);
            preStmt.setInt(1, born);
 
            rs = preStmt.executeQuery();
            while (rs.next()) {
                Map n = rs.getObject("p", Map.class);
                System.out.print("name: " + n.get("name"));
                System.out.println("  born: " + n.get("born"));
 
            }
        }
    }
}

上記のように、Cypher文を定義して、実行結果をResultSetで受けます。
正直、ResultSet,PreparedStatementを使うのは辛すぎるのでオススメ出来ないです。
(戻り値もMapだし。。。)

GitHubのREADMEにあるように、GUIツールのドライバーに使用するのが、正しい使い方だと思います。

Neo4j Object Graph Mapper(OGM)

グラフDB向けのORMと考えてください。

Neo4j OGM

https://github.com/neo4j/neo4j-ogm
http://neo4j.com/docs/ogm/java/stable/

Ruby版:https://rubygems.org/gems/neo4j/

使うのであれば、SDN4の方が良いですが、SDN4もOGMベースなのでOGMの基本的内容をやります。

引き続き、サンプルデータ「Movie」を使用します。


src
└── main
├── java
│   └── jp
│   └── co
│   └── inte
│   ├── Neo4JOGMSample.java
│   ├── Neo4jSessionFactory.java
│   ├── domain
│   │   ├── nodes
│   │   │   ├── Movie.java
│   │   │   └── Person.java
│   │   └── relationships
│   │   ├── Acted.java
│   │   └── Directed.java
│   └── service
│   ├── GenericService.java
│   ├── PersonService.java
│   ├── Service.java
│   └── impl
│   └── PersonServiceImpl.java
└── resources

依存関係は以下のとおりです。

dependencies {
compile 'org.neo4j:neo4j:2.3.1'
compile 'org.neo4j:neo4j-ogm:1.1.3'
}

Neo4JOGMSample.java

package jp.co.inte;
 
import jp.co.inte.domain.nodes.Person;
import jp.co.inte.domain.relationships.Acted;
import jp.co.inte.service.PersonService;
import jp.co.inte.service.impl.PersonServiceImpl;
 
import java.util.List;
import java.util.Optional;
 
/**
 * 1956年生まれの俳優/女優の名前と、出演した映画のタイトルを出演します。
 */
public class Neo4JOGMSample {
 
    public static void main(String... args) {
        PersonService service = new PersonServiceImpl();
 
        Iterable p = service.findByBorn(1956);
        p.forEach(person -> {
            System.out.print(person.getName());
 
            // Personノードから「ACTED_IN」でリレーションシップされているMovieノードを取得
            List acteds = person.getActed();
            acteds.forEach( acted -> {
                System.out.print(" : " + acted.getMovie().getTitle());
            });
 
            System.out.println();
        });
    }
}

Neo4jSessionFactory.java

package jp.co.inte;
 
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
 
public class Neo4jSessionFactory {
    // Entityクラスがあるパッケージ名を渡す
    private final static SessionFactory sessionFactory = new SessionFactory("jp.co.inte.domain.nodes", "jp.co.inte.domain.relationships");
    private static Neo4jSessionFactory factory = new Neo4jSessionFactory();
 
    public static Neo4jSessionFactory getInstance() {
        return factory;
    }
 
    private Neo4jSessionFactory() {
    }
 
    public Session getNeo4jSession() {
        return sessionFactory.openSession("http://localhost:7474", "neo4j", "neo");
    }
 
}

Service.java

package jp.co.inte.service;
 
public interface Service<T> {
 
    Iterable<T> findAll();
 
    T find(Long id);
 
}

GenericService.java

package jp.co.inte.service;
 
import jp.co.inte.Neo4jSessionFactory;
import org.neo4j.ogm.session.Session;
 
public abstract class GenericService<T> implements Service<T> {
    private static final int DEPTH_LIST = 0;
    private static final int DEPTH_ENTITY = 2;
    private Session session = Neo4jSessionFactory.getInstance().getNeo4jSession();
 
    @Override
    public Iterable<T> findAll() {
        return session.loadAll(getEntityType(), DEPTH_LIST);
    }
 
    @Override
    public T find(Long id) {
        return session.load(getEntityType(), id, DEPTH_ENTITY);
    }
 
    public abstract Class<T> getEntityType();
}

PersonService.java

package jp.co.inte.service;
 
import jp.co.inte.domain.nodes.Person;
 
public interface PersonService {
    public Iterable<Person> findByBorn(int born);
    public Iterable<Person> findAll();
}

PersonServiceImpl.java

package jp.co.inte.service.impl;
 
import org.neo4j.ogm.session.Session;
 
import jp.co.inte.Neo4jSessionFactory;
import jp.co.inte.domain.nodes.Person;
import jp.co.inte.service.GenericService;
import jp.co.inte.service.PersonService;
 
import java.util.HashMap;
import java.util.Map;
 
public class PersonServiceImpl extends GenericService<Person> implements PersonService {
 
    @Override
    public Iterable<Person> findByBorn(int born) {
 
        /**
         * リレーションシップも返さないとNodeEntityクラスのRelationshipに値が入らないので注意
         */
        String query = "MATCH (p:Person)-[a:ACTED_IN|DIRECTED]->() where p.born = {born} return p,a";
 
 
        Session session = Neo4jSessionFactory.getInstance().getNeo4jSession();
        Map<String, Integer> params = new HashMap<>();
        params.put("born", born);
 
        return session.query(getEntityType(), query, params);
    }
 
    @Override
    public Class<Person> getEntityType() {
        return Person.class;
    }
}

Movie.java

package jp.co.inte.domain.nodes;
 
import org.neo4j.ogm.annotation.GraphId;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Property;
import org.neo4j.ogm.annotation.Relationship;
 
/*
 * アノテーションでLABEL、ノードのプロパティ、リレーションシップタイプなどを指定します。
 */
@NodeEntity(label = "Movie")
public class Movie {
 
    @GraphId
    private Long id;
 
    @Property(name = "title")
    private String title;
 
    @Property(name = "tagline")
    private String tagline;
 
    @Property(name = "released")
    private Integer released;
 
    @Relationship(type = "ACTED_IN", direction = "INCOMING")
    private Person person;
 
/**
Getter、Setterは省略
**/
}

Person.java

package jp.co.inte.domain.nodes;
 
import org.neo4j.ogm.annotation.GraphId;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Property;
import org.neo4j.ogm.annotation.Relationship;
import jp.co.inte.domain.relationships.Acted;
import jp.co.inte.domain.relationships.Directed;
 
import java.util.ArrayList;
import java.util.List;
 
@NodeEntity(label = "Person")
public class Person {
 
    @GraphId
    private Long id;
 
    @Property(name = "name")
    private String name;
 
    @Property(name = "born")
    private Integer born;
 
    @Relationship(type = "ACTED_IN", direction = "OUTGOING")
    private List<Acted> acted  = new ArrayList<>();
 
    public List<Acted> getActed() {
        return acted;
    }
 
    public void setActed(List<Acted> acted) {
        this.acted = acted;
    }
 
    public void setActed(Acted acted) {
        this.acted.add(acted);
    }
 
    @Relationship(type = "DIRECTED")
    private List<Directed> directed = new ArrayList<>();
 
    public List<Directed> getDirected() {
        return directed;
    }
 
    public void setDirected(List<Directed> directed) {
        this.directed = directed;
    }
}

Acted.java

package jp.co.inte.domain.relationships;
 
import jp.co.inte.domain.nodes.Movie;
import jp.co.inte.domain.nodes.Person;
import org.neo4j.ogm.annotation.*;
 
import java.util.List;
 
/*
 * リレーションシップのエンティティでは、開始、終了ノードもアノテーションで指定できます。
 */
@RelationshipEntity(type = "ACTED_IN")
public class Acted {
    @GraphId
    private Long id;
 
    @Property(name = "roles")
    private List<String> roles;
 
    @StartNode private Person person;
    @EndNode private Movie movie;
 
/**
略
*/
}

Directed.java

package jp.co.inte.domain.relationships;
 
import jp.co.inte.domain.nodes.Movie;
import jp.co.inte.domain.nodes.Person;
import org.neo4j.ogm.annotation.*;
 
@RelationshipEntity(type = "DIRECTED")
public class Directed {
    @GraphId
    private Long id;
 
    @StartNode private Person person;
    @EndNode private Movie movie;
 
/**
略
*/
}

Entityクラスなど下準備が面倒な点がありますが、アプリケーションを開発するうえでは、やりやすのではないでしょうか。
今回は取得しか行いませんでしたが、登録、更新もORMを使う感覚で行えます。
また、SND4に、Neo4jの商用サービスを展開しているGraphAware社のエンジニアがフルコミットで開発に参加しているようなので、今後の展開を非常に注目しています。

次回

書籍によるとREST APIは、組み込みAPIに比べて10倍遅いらしいです。
通信とか解析・変換があるので仕方ないかと。

そこで次回はServer PluginとUnmanaged Extensionsにチャレンジ!!します。