Chapter 2. Tutorial

This chapter is a tutorial that takes the reader through steps necessary to get started with the Neo4j-OGM.

2.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.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.

Graph model

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.

2.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.

2.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.

Gradle dependencies for Neo4j-OGM. 

compile "org.neo4j:neo4j-ogm-core:3.2.1"
runtime "org.neo4j:neo4j-ogm-bolt-driver:3.2.1"

Refer to Dependency Management for more information on dependencies.

2.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");

2.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.

2.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;
}

2.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;
}

2.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;

}

2.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
Warning

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() {

    }
}

2.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.

2.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() {
    }
}

2.5. Interacting with the model

So our domain entities are annotated, now we’re ready persist them to the graph!

2.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.

Note

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.

Class detail

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.

Department listing

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.

2.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.

2.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.