OGM込みでNeo4jを使っていきます
Kotlinを書きたかったのでときどきKotlinが混じっていますが大人しくJavaを使ったほうがいいと思います
Neo4jとは
Meet Neo4j: The graph database platform powering today's mission-critical enterprise applications, including artificial intelligence, fraud detection and recommendations.
driverライブラリだけで使うこともできる。
Maven Repository: org.neo4j.driver » neo4j-java-driver
今回はOGMとBoltを使う。
Maven Repository: org.neo4j » neo4j-ogm
Maven Repository: org.neo4j » neo4j-ogm-bolt-driver
DockerにNeo4j環境の用意がある。
library/neo4j - Docker Hub
docker run \
--publish=7474:7474 --publish=7687:7687 \
--volume=$HOME/neo4j/data:/data \
--volume=$HOME/neo4j/logs:/logs \
neo4j
上記ポートの場合、
ブラウザで http://localhost:7474/ を見ると色々と遊べる。
boltは bolt://localhost:7687/ から。
パスワードはブラウザでの初回ログイン時に決定するか、 --env NEO4J_AUTH=neo4j/<password>
をつけて設定する。
とりあえず試す
@NodeEntity
public class Movie {
@Id Long id;
Movie(Long id) {
this.id = id;
}
}
public static void main(String[] args) {
Configuration configuration = new Configuration.Builder()
.uri("bolt://適当なuri")
.credentials(ユーザネーム, パスワード)
.build();
SessionFactory sessionFactory = new SessionFactory(configuration, パッケージ);
sessionFactory.openSession().save(new Movie(0L));
}
パッケージにはMovie.javaがあるところのパッケージ名を書く。
本格的に作り込む
オブジェクトのマッピングとデータベースへの接続ができるように用意しなければならない。
Entityを作る
ノードに相当するNodeEntityを作成する。エッジに個別の情報を持たせるにはRelationshipEntityを作成する。作ったEntityのあるパッケージを SessionFactory
に渡してセッションを作ることで、OGMがEntityを認識する。
SessionFactory
にはパッケージを可変長引数で受け取るコンストラクタがあるので、複数のパッケージに分けて作ってしまっても大丈夫。
作り間違えていると実行時にエラーが出る。
NodeEntityを作る
abstract class Entity {
@Id @GeneratedValue
private Long id;
public Long getId() {
return id;
}
}
@NodeEntity
class Course extends Entity {
String name;
@Relationship(type= "SUBJECT_TAUGHT")
Subject subject;
@Relationship(type= "TEACHES_CLASS", direction=Relationship.INCOMING)
Teacher teacher;
@Relationship(type= "ENROLLED", direction=Relationship.INCOMING)
Set<Enrollment> enrollments = new HashSet<>();
}
NodeEntityとするクラスには、
-
@NodeEntity
をつける。 - primary idが必要。primary idとするフィールドには
@Id
をつける。- 例のようにabstract classでつけてもOK。
-
@GeneratedValue
をつけるとcreate時に勝手に値を決めてくれる。
- エッジを張りたいときは相手のNodeEntityの型を持つフィールドを用意して
@Relationship
をつける。- 対多数の関係は
Set<>
にする。 -
type
は相手ノードのフィールドと一致させる。 -
direction = Relationship.INCOMING
でエッジの向きをこのノードに向けることができる。初期値ではRelationship.OUTGOING
になっている。
- 対多数の関係は
RelationshipEntityを作る
エッジに何か詳しく情報を持たせたい場合はクラスを作成し、その型のフィールドをノードに持たせる。
@RelationshipEntity(type = "ENROLLED")
class Enrollment {
@StartNode
Student student;
@EndNode
Course course;
Date enrolledDate;
}
-
type
はNodeEntityのフィールドで指定するものと合わせる。 - 向きに合わせて
@StartNode
にあたるNodeEntityの型のフィールドと@EndNode
にあたるノードのフィールドを作る。
Entityの使用例
// NodeEntityのインスタンスを作成
val course = Course(/*適当に作成*/)
val student = Student(/*適当に作成*/)
// RelationshipEntityのインスタンスを作成
val enrollment = Enrollment(student: student, course: course, /*適当に作成*/)
// NodeEntityにRelationshipの情報を与える
course.enrollments = hashSetOf(enrollment)
student.enrollments = hashSetOf(enrollment)
session.save(course)
どう頑張ってもRelationshipのフィールドはNodeEntityインスタンス作成時に埋まらないので、一旦nullかなにかで埋めておくしかなさそう。Kotlinにはちょっとつらい。
ついでにServiceも作ってみる
Session
に用意されているメソッドを直接呼ぶだけでも十分使うことができるが、公式が Service
を作っていたのでKotlin版に書き換えてみた。
interface Entity {
Long getId();
}
implements Entity
して @Id Long id;
としておけばOK。
interface Service<T> {
fun findAll(): Iterable<T>
fun find(id: Long): T?
fun find(vararg filters: Filter): Iterable<T>
fun delete(id: Long)
fun createOrUpdate(`object`: T): T?
}
abstract class GenericService<T: Entity>(
private val session: Session
): Service<T> {
companion object {
private const val DEPTH_LIST = 0
private const val DEPTH_ENTITY = 1
}
override fun find(id: Long): T? {
return session.load(getEntityType(), id, DEPTH_ENTITY)
}
override fun find(vararg filters: Filter): Iterable<T> {
return session.loadAll(getEntityType(), Filters(filters.toList()))
}
override fun findAll(): Iterable<T> {
return session.loadAll(getEntityType(), DEPTH_LIST)
}
override fun createOrUpdate(`object`: T): T? {
session.save(`object`, DEPTH_ENTITY)
return find(`object`.id)
}
override fun delete(id: Long) {
session.delete(session.load(getEntityType(), id))
}
abstract fun getEntityType(): Class<T>
}
class HogeService(session: Session): GenericService<Hoge>(session) {
override fun getEntityType(): Class<Hoge> {
return Hoge::class.java
}
}
テストする
Maven Repository: org.neo4j » neo4j-ogm-test
にある Neo4jRule
が便利。
class HogeTest {
@Rule @JvmField
val neo4jRule = Neo4jRule()
private lateinit var session: Session
@Before
fun setUp() {
val configuration = Configuration.Builder()
.uri(neo4jRule.boltURI().toString())
.build()
val sessionFactory = SessionFactory(configuration, Movie::class.java.`package`.name)
session = sessionFactory.openSession()
session.purgeDatabase()
}
@Test
fun hogeTest() {
// ご自由に
}
}
その他補足
- OGMの中のSLF4Jが「Failed to load class "org.slf4j.impl.StaticLoggerBinder".」と言ってくるので、お好みでライブラリを追加する。
- 実行時にようやくEntityに関するエラーが出るので、Entity専用のパッケージを用意してちょっとずつ作っていくとまだマシ。
- LombokでEntityに
@Builder
@Builder.Default
@Getter
@Setter
を与えておくと楽かもしれない。 - 上手にOGMを使いたい人向け資料: Chapter 3. Reference - Neo4j OGM - An Object Graph Mapping Library for Neo4j v3.1
参考文献
Using Neo4j from Java - Neo4j Graph Database Platform
Neo4j OGM - An Object Graph Mapping Library for Neo4j v3.1
Neo4j with Docker - Neo4j Graph Database Platform
2.5. Docker - Chapter 2. Installation
neo4j-examples/neo4j-ogm-university: Example Project for Neo4j OGM
neo4j-sdn-ogm-issue-report-template/ogm-3.0 at master · neo4j-examples/neo4j-sdn-ogm-issue-report-template
最初はKotlinだけで全部書こうとしたのだが、EntityのフィールドがnullになりがちであることとLombokの使用を優先したので、
- EntityだけJavaで書き、
- Session周りはKotlinで書き、
- 別モジュールからKotlinでばりばり使う
になってしまった。悲しかった。