Tutorial
This chapter is a tutorial that takes the reader through steps necessary to get started with the Neo4j-OGM.
1. Introduction
Neo4j-OGM University is a demo application for the Neo4j-OGM library that allows you to manage the Departments, Teaching Staff, Subjects, Students and Classes of a fictitious educational institution: Hilly Fields Technical College.
It is a fully functioning web-application built using the following components:
-
Groovy
-
Ratpack
-
Neo4j-OGM
-
AngularJS
-
Bootstrap
The application’s architecture involves a RESTful server interfacing with a rich single page application that is designed to show off the performance and capabilities of Neo4j-OGM.
The complete source code for the application is available on Github.
2. Building the domain model
Before we get to any code, we want to whiteboard our graph model.
Our college will contain departments
, each of which offer various subjects
taught by a teacher
.
Students
enroll
for courses
or classes
that teach a subject
.
We’re also going to model a study buddy
which represents a group of students
that get together to help one another study for a class
.
Here’s what we came up with.

When we translate this model to Groovy, it ends up being pretty straightforward:
class Department {
String name;
Set<Subject> subjects;
}
class Subject {
String name;
Department department;
Set<Teacher> teachers;
Set<Course> courses;
}
class Teacher {
String name;
Set<Course> courses;
Set<Subject> subjects;
}
class Course {
String name;
Subject subject;
Teacher teacher;
Set<Enrollment> enrollments;
}
class Student {
String name;
Set<Enrollment> enrollments;
Set<StudyBuddy> studyBuddies;
}
class Enrollment {
Student student;
Course course;
Date enrolledDate;
}
When a student enrolls for a course, we’re also going to keep track of the enrollment date.
In the model, this will be stored as a property on the ENROLLED
relationship between a student and a course.
This kind of rich relationship is managed by the class Enrollment
and is known as a relationship entity.
The important thing to take away here is that Neo4j-OGM supports Neo4j’s philosophy of whiteboard friendly domain models. By focusing on the model the code almost writes itself.
3. Configuring Neo4j-OGM
Neo4j-OGM supports several drivers:
-
Bolt - the lightning fast native driver for Neo4j.
-
HTTP - the original transactional HTTP endpoint for remote Neo4j deployments.
-
Embedded - for embedded deployments within a Java application.
Our sample application will use the Bolt driver.
3.1. Setting up with Gradle
The demo application uses Gradle as a build system.
Before we can use the library, we need to add a dependency.
compile "org.neo4j:neo4j-ogm-core:3.2"
runtime "org.neo4j:neo4j-ogm-bolt-driver:3.2"
Refer to Dependency Management for more information on dependencies.
3.2. Connecting to the database
We configure the database parameters by using the configuration builder.
Configuration configuration = new Configuration.Builder()
.uri("bolt://localhost")
.credentials("neo4j", "password")
.build();
SessionFactory sessionFactory = new SessionFactory(configuration, "com.mycompany.app.domainclasses");
4. Annotating the domain model
Much like Hibernate or JPA, Neo4j-OGM allows you to annotate your POJOs in order to map them to nodes, relationships and properties in the graph.
4.1. Node Entities
POJOs annotated with @NodeEntity
will be represented as nodes in the graph.
The label assigned to this node can be specified via the label
property on the annotation; if not specified, it will default to the simple class name of the entity.
Each parent class in addition also contributes a label to the entity (with the exception of java.lang.Object
).
This is useful when we want to retrieve collections of super types.
Let’s go ahead and annotate all our node entities in the code we wrote earlier.
Note that we’re overriding the default label for a Course
with Class
@NodeEntity
class Department {
String name;
Set<Subject> subjects;
}
@NodeEntity
class Subject {
String name;
Department department;
Set<Teacher> teachers;
Set<Course> courses;
}
@NodeEntity
class Teacher {
String name;
Set<Course> courses;
Set<Subject> subjects;
}
@NodeEntity(label="Class")
class Course {
String name;
Subject subject;
Teacher teacher;
Set<Enrollment> enrollments;
}
@NodeEntity
class Student {
String name;
Set<Enrollment> enrollments;
Set<StudyBuddy> studyBuddies;
}
4.2. Relationships
Next up, the relationships between the nodes.
Every field in an entity that references another entity is backed by a relationship in the graph.
The @Relationship
annotation allows you to specify both the type of the relationship and the direction.
By default, the direction is assumed to be OUTGOING
and the type is the UPPER_SNAKE_CASE field name.
We’re going to be specific about the relationship type to avoid using the default and also make it easier to refactor classes later by not being dependent on the field name. Again, we are going to modify the code we saw in the last section:
@NodeEntity
class Department {
String name;
@Relationship(type = "CURRICULUM")
Set<Subject> subjects;
}
@NodeEntity
class Subject {
String name;
@Relationship(type="CURRICULUM", direction = Relationship.INCOMING)
Department department;
@Relationship(type = "TAUGHT_BY")
Set<Teacher> teachers;
@Relationship(type = "SUBJECT_TAUGHT", direction = "INCOMING")
Set<Course> courses;
}
@NodeEntity
class Teacher {
String name;
@Relationship(type="TEACHES_CLASS")
Set<Course> courses;
@Relationship(type="TAUGHT_BY", direction = Relationship.INCOMING)
Set<Subject> subjects;
}
@NodeEntity(label="Class")
class Course {
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
class Student {
String name;
@Relationship(type = "ENROLLED")
Set<Enrollment> enrollments;
@Relationship(type = "BUDDY", direction = Relationship.INCOMING)
Set<StudyBuddy> studyBuddies;
}
4.3. Relationship Entities
Sometimes something isn’t quite a Node entity.
In this demo the only remaining class to annotate is Enrollment
.
As discussed earlier, this is a relationship entity since it manages the underlying ENROLLED
relation between a student and course.
It isn’t a simple relation because it has a relationship property called enrolledDate
.
A relationship entity must be annotated with @RelationshipEntity
and also the type of relationship.
In this case, the type of relationship is ENROLLED
as specified in both the Student
and Course
entities.
We are also going to indicate to Neo4j-OGM the start and end node of this relationship.
@RelationshipEntity(type = "ENROLLED")
class Enrollment {
@StartNode
Student student;
@EndNode
Course course;
Date enrolledDate;
}
4.4. Identifiers
Every node and relationship persisted to the graph must have an id. Neo4j-OGM uses this to identify and re-connect the entity to the graph in memory. Identifier may be either a primary id or a native graph id.
-
primary id - any property annotated with
@Id
, set by the user and optionally with@GeneratedValue
annotation -
native id - this id corresponds to the id generated by the Neo4j database when a node or relationship is first saved, must be of type
Long
Do not rely on native id for long running applications. Neo4j will reuse deleted node id’s. It is recommended users come up with their own unique identifier for their domain objects (or use a UUID). |
Since every entity requires an id, we’re going to create an Entity
superclass.
This is an abstract class, so you’ll see that the nodes do not inherit an Entity
label, which is exactly what we want.
If you plan on implementing hashCode
and equals
make sure it does not make use of the native id.
See Node Entities for more information.
abstract class Entity {
@Id @GeneratedValue
private Long id;
public Long getId() {
return id;
}
}
Our entities will now extend this class, for example
@NodeEntity
class Department extends Entity {
String name;
@Relationship(type = "CURRICULUM")
Set<Subject> subjects;
Department() {
}
}
4.5. No Arg Constructor
We are almost there!
Neo4j-OGM also also requires a public no-args constructor to be able to construct objects from all our annotated entities. We’ll make sure all our entities have one.
4.6. Converters
Neo4j supports Numeric
, String
, boolean
and arrays of these as property values.
How do we handle the enrolledDate
since Date
is not a valid data type?
Luckily for us, Neo4j-OGM provides many converters out of the box, one of which is a Date
to Long
converter.
We simply annotate the field with @DateLong
and the conversion of the Date
to it’s Long
representation and back is handled by Neo4j-OGM when persisting and loading from the graph.
@RelationshipEntity(type = "ENROLLED")
class Enrollment {
Long id;
@StartNode
Student student;
@EndNode
Course course;
@DateLong
Date enrolledDate;
Enrollment() {
}
}
5. Interacting with the model
So our domain entities are annotated, now we’re ready persist them to the graph!
5.1. Sessions
The smart object mapping capability is provided by the Session
object.
A Session
is obtained from a SessionFactory
.
We’re going to set up the SessionFactory
just once and have it produce as many sessions as required.
public class Neo4jSessionFactory {
private final static Configuration = ... // provide configuration as seen before
private final static SessionFactory sessionFactory = new SessionFactory(configuration, "school.domain");
private static Neo4jSessionFactory factory = new Neo4jSessionFactory();
public static Neo4jSessionFactory getInstance() {
return factory;
}
// prevent external instantiation
private Neo4jSessionFactory() {
}
public Session getNeo4jSession() {
return sessionFactory.openSession();
}
}
The SessionFactory
constructor accepts packages that are to be scanned for annotated domain entities.
The domain objects in our university application are grouped under school.domain
.
When the SessionFactory
is created, it will scan school.domain
for potential domain classes and construct the object mapping metadata to be used by all sessions created thereafter.
We use here the SessionFactory with the package of domain classes as a parameter.
This sets up an in-memory embedded database.
In your application, you would also pass the configuration to connect to your actual database.
|
The Session
keeps track of changes made to entities and relationships and persists ones that have been modified on save.
Once an entity is tracked by the session, reloading this entity within the scope of the same session will result in the session cache returning the previously loaded entity.
However, the subgraph in the session will expand if the entity or its related entities retrieve additional relationships from the graph.
For the purpose of this demo application, we’ll use short living sessions - a new session per web request - to avoid stale data issues.
Our university application will use the following operations:
interface Service<T> {
Iterable<T> findAll()
T find(Long id)
void delete(Long id)
T createOrUpdate(T object)
}
These CRUD interactions with the graph are all handled by the Session
.
Let’s write a GenericService
to deal with common Session
operations.
abstract class GenericService<T> implements Service<T> {
private static final int DEPTH_LIST = 0
private static final int DEPTH_ENTITY = 1
protected Session session = Neo4jSessionFactory.getInstance().getNeo4jSession()
@Override
Iterable<T> findAll() {
return session.loadAll(getEntityType(), DEPTH_LIST)
}
@Override
T find(Long id) {
return session.load(getEntityType(), id, DEPTH_ENTITY)
}
@Override
void delete(Long id) {
session.delete(session.load(getEntityType(), id))
}
@Override
T createOrUpdate(T entity) {
session.save(entity, DEPTH_ENTITY)
return find(entity.id)
}
abstract Class<T> getEntityType()
}
One of the features of Neo4j-OGM is variable depth persistence.
This means you can vary the depth of fetches depending on the shape of your data and application.
The default depth is 1, which loads simple properties of the entity and its immediate relations.
This is sufficient for the find
method, which is used in the application to present a create or edit form for an entity.

Loading relationships is not required when listing all entities of a type.
We merely require the id and name of the entity, and so a depth of 0 is used by findAll
to only load simple properties of the entity but skip its relationships.

The default save depth is -1, or everything that has been modified and can be reached from the entity up to an infinite depth. This means we can persist all our changes in one go.
This GenericService
takes care of CRUD operations for all our entities!
All we did was delegate to the Session
; no need to write persistence logic for every entity.
5.2. Queries
Popular Study Buddies is a report that lists the most popular peer study groups.
This requires a custom Cypher query.
It is easy to supply a Cypher query to the query
method available on the Session
.
class StudyBuddyServiceImpl extends GenericService<StudyBuddy> implements StudyBuddyService {
@Override
Iterable<StudyBuddy> findAll() {
return session.loadAll(StudyBuddy, 1)
}
@Override
Iterable<Map<String, Object>> getStudyBuddiesByPopularity() {
String query = "MATCH (s:StudyBuddy)<-[:BUDDY]-(p:Student) return p, count(s) as buddies ORDER BY buddies DESC"
return Neo4jSessionFactory.getInstance().getNeo4jSession().query(query, Collections.EMPTY_MAP)
}
@Override
Class<StudyBuddy> getEntityType() {
return StudyBuddy.class
}
}
The query
provided by the Session
can return a domain object, a collection of them, or a special wrapped object called a Result
.
6. Conclusion
With not much effort, we’ve built all the services that tie together this application. All that is required is adding controllers and building the UI. The fully functioning application is available at Github.
We encourage you to read the reference guide that follows and apply the concepts learned by forking the application and adding to it.
Was this page helpful?