Object Graph Mapping by Example with Neo4j OGM and Java

July 18th, 2016 by

When integrating a Neo4j database into a Java application a developer often needs to map nodes and edges of the graph to corresponding Java classes of the domain model.

Neo4j OGM eases this work and allows us to map our domain objects to the graph database using simple annotations – similar to the Java Persistence API (JPA) for relational database management systems.

In the following tutorial I’d like to demonstrate how to use Neo4j OGM to build a simple train timetable planner and a permission system mapping between graph, nodes, edges and POJOs.

Gephi rendering a Graph

Gephi rendering a Graph

 

Dependencies

Using Gradle as a build tool here we just need to add the two dependencies for neo4j-ogm-core and neo4j-org-embedded-driver to our project’s build.gradle file.

In addition I have added a task to execute a selected Java main class via command-line.

apply plugin: 'java'
apply plugin: 'eclipse'
 
sourceCompatibility = 1.8
 
repositories {
    mavenCentral()
}
 
dependencies {
    compile 'org.neo4j:neo4j-ogm-core:2.0.3'
    compile 'org.neo4j:neo4j-ogm-embedded-driver:2.0.3'
}
 
task execute(type:JavaExec) {
    main = mainClass
    classpath = sourceSets.main.runtimeClasspath
}

Example 1: Train Timetable

In our first example, we’re re-using the train timetable example from my previous tutorial “Neo4j Graph Database Tutorial: How to build a Route Planner and other Examples“.

In this example, we have a directed graph where the nodes are train-stations and the edges are the connections between these stations with a property marking the distance between two nodes.

Directed graph of train-stations and their connections

Directed graph of train-stations and their connections

Entities

To transfer this graph to a Java domain model, we need to create two specific entities .. for train-stations and their connections.

TrainStation

This is our train-station entity. Every entity needs to have a primary key. We may use an id attribute of type Long here or otherwise mark our primary key with @GraphId.

package com.hascode.tutorial.entity;
 
import java.util.HashSet;
import java.util.Set;
 
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;
 
@NodeEntity
public class TrainStation {
    private Long id;
 
    private String name;
 
    @Relationship(type = Connection.TYPE, direction = Relationship.OUTGOING)
    private Set<Connection> connections = new HashSet<>();
 
    public TrainStation(String name) {
        this.name = name;
    }
 
    public void addConnection(TrainStation target, int distance) {
        this.connections.add(new Connection(this, target, distance));
    }
 
    // hashCode, equals, toString, getter/setter, no-arg constructor ommitted..
}
Connection

This entity represents a connection between to stations and it has an attribute distance that we need to calculate the shortest route between two stations.

Starting and ending stations are annotated with @StartNode and @EndNode and the class itself is marked as a relationship entity with @RelationshipEntity.

package com.hascode.tutorial.entity;
 
import org.neo4j.ogm.annotation.EndNode;
import org.neo4j.ogm.annotation.RelationshipEntity;
import org.neo4j.ogm.annotation.StartNode;
 
@RelationshipEntity(type = Connection.TYPE)
public class Connection {
    public static final String TYPE = "LEADS_TO";
 
    private Long id;
 
    @StartNode
    private TrainStation start;
 
    @EndNode
    private TrainStation end;
 
    private int distance;
 
    public Connection(TrainStation start, TrainStation end, int distance) {
        this.start = start;
        this.end = end;
        this.distance = distance;
    }
 
    // hashCode, equals, toString, getter/setter, no-arg constructor ommitted..
}

Driver Configuration

We need to tell Neo4j which driver to use, either HTTP Driver or the Embedded Driver. For the following examples, we’ll be using the embedded driver without a persistent data store.

Property Config File

To create out configuration we simply  need to add the following property file named ogm.properties to our application’s classpath:

driver=org.neo4j.ogm.drivers.embedded.driver.EmbeddedDriver
Alternative: Java Configuration

Otherwise it is also possible to create the configuration programmatically and pass the modified configuration object to the SessionFactory‘s constructor like this:

import org.neo4j.ogm.config.Configuration;
[..]
 
Configuration config = new Configuration();
config.driverConfiguration().setDriverClassName("org.neo4j.ogm.drivers.embedded.driver.EmbeddedDriver");
SessionFactory sessionFactory = new SessionFactory(config, "com.hascode");
[..]

Application

Now this is our train timetable application, executing the following steps:

  • create a new session-factory, a session and open a transaction
  • create multiple train-stations
  • create multiple connections between these train-stations
  • persist train-stations and connections the the graph database
  • calculate and print the shortest route between London and Bristol using a Cypher query
  • calculate and print the shortest route between London and Southampton using a Cypher query
package com.hascode.tutorial.boundary;
 
import java.util.Collections;
 
import org.neo4j.graphdb.Path;
import org.neo4j.ogm.model.Result;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.neo4j.ogm.transaction.Transaction;
 
import com.hascode.tutorial.entity.TrainStation;
 
public class TrainTimetableExample {
    public static void main(String[] args) {
 
        SessionFactory sessionFactory = new SessionFactory("com.hascode");
        final Session session = sessionFactory.openSession();
        Transaction tx = session.beginTransaction();
 
        TrainStation london = new TrainStation("London");
        TrainStation brighton = new TrainStation("Brighton");
        TrainStation portsmouth = new TrainStation("Portsmouth");
        TrainStation bristol = new TrainStation("Bristol");
        TrainStation oxford = new TrainStation("Oxford");
        TrainStation gloucester = new TrainStation("Gloucester");
        TrainStation northampton = new TrainStation("Northampton");
        TrainStation southampton = new TrainStation("Southampton");
 
        london.addConnection(brighton, 52);
        brighton.addConnection(portsmouth, 49);
        portsmouth.addConnection(southampton, 20);
        london.addConnection(oxford, 95);
        oxford.addConnection(southampton, 66);
        oxford.addConnection(northampton, 45);
        northampton.addConnection(bristol, 114);
        southampton.addConnection(bristol, 77);
        northampton.addConnection(gloucester, 106);
        gloucester.addConnection(bristol, 35);
 
        session.save(london);
        session.save(brighton);
        session.save(portsmouth);
        session.save(bristol);
        session.save(oxford);
        session.save(gloucester);
        session.save(northampton);
        session.save(southampton);
 
        System.out.println(session.countEntitiesOfType(TrainStation.class) + " stations saved");
        getRoute("London", "Bristol", session);
        getRoute("London", "Southampton", session);
 
        tx.close();
    }
 
    private static void getRoute(final String from, final String destination, final Session session) {
        System.out.printf("searching for the shortest route from %s to %s..\n", from, destination);
 
        String cypherQuery = String.format(
                "MATCH (from:TrainStation {name:'%s'}), (to:TrainStation {name:'%s'}), paths=allShortestPaths((from)-[:LEADS_TO*]->(to))
                 WITH REDUCE(dist = 0, rel in rels(paths) | dist + rel.distance) AS distance, paths
                 RETURN paths, distance
                 ORDER BY distance
                 LIMIT 1",
                from, destination);
        final Result result = session.query(cypherQuery, Collections.emptyMap());
        System.out.printf("shortest way from %s to %s via\n", from, destination);
        result.queryResults().forEach(entry -> {
            Long distance = (Long) entry.get("distance");
            Path path = (Path) entry.get("paths");
            System.out.printf("distance: %s\n", distance);
            path.nodes().forEach(node -> {
                System.out.printf("- %s\n", node.getProperty("name"));
            });
        });
    }
}

Cypher Query for Path Calculation

This is our Cypher query to calculate the shortest path between two train-stations using the distance attribute of the connection relation.

For more details about the features and possibilities of the Cypher query language, please feel free to take a closer look at their excellent documentation here.

MATCH (FROM:TrainStation {name:"London"}),
      (TO:TrainStation {name:"Bristol"}),
      paths=allShortestPaths((FROM)-[:LEADS_TO*]->(TO))
WITH REDUCE(dist = 0, rel IN rels(paths) | dist + rel.distance) AS distance, paths
RETURN paths, distance
ORDER BY distance
LIMIT 1

Running the Example

We’re now ready to run our application using our IDE of choice or using the command-line-interface with Gradle and our special Gradle task “execute” like this:

$ gradle -PmainClass=com.hascode.tutorial.boundary.TrainTimetableExample execute
8 stations saved
searching for the shortest route from London to Bristol..
shortest way from London to Bristol via
distance: 198
- London
- Brighton
- Portsmouth
- Southampton
- Bristol
searching for the shortest route from London to Southampton..
shortest way from London to Southampton via
distance: 121
- London
- Brighton
- Portsmouth
- Southampton
 
BUILD SUCCESSFUL
 
Total time: 6.288 secs

Example 2: Complex Permission System

Similar to the example in my Neo4j tutorial “How to build a route planner and other examples” we’re creating a complex permission system consisting of users, groups, roles and permissions.

Users may be member of a group, group may have permissions or may be assigned to roles, roles may have permissions, too.

Permission graph visualized

Permission graph visualized

Entities

So we need to model three simple entities: users, groups, roles and permissions. As our relations do not have any special attributes, we don’t need to create a special class for our relations here.

User

This is our user entity, a user has ..

  • a name
  • zero or more groups
package com.hascode.tutorial.entity;
 
import java.util.HashSet;
import java.util.Set;
 
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;
 
@NodeEntity
public class User {
    private Long id;
 
    private String name;
 
    @Relationship(direction = Relationship.OUTGOING, type = "IS_MEMBER")
    private Set<Group> groups = new HashSet<>();
 
    public User(String name) {
        this.name = name;
    }
 
    // hashCode, equals, toString, getter/setter, no-arg constructor ommitted..
}
Group

Our group entity has these specific attributes

  • a name
  • zero or more permissions
  • zero or more roles
package com.hascode.tutorial.entity;
 
import java.util.HashSet;
import java.util.Set;
 
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;
 
@NodeEntity
public class Group {
    private Long id;
 
    private String name;
 
    @Relationship(direction = Relationship.OUTGOING, type = "HAS_PERMISSION")
    private Set<Permission> permissions = new HashSet<>();
 
    @Relationship(direction = Relationship.OUTGOING, type = "HAS_ROLE")
    private Set<Role> roles = new HashSet<>();
 
    public Group(String name) {
        this.name = name;
    }
 
    // hashCode, equals, toString, getter/setter, no-arg constructor ommitted..
}
Role

Our role entity has..

  • a name
  • zero or more permissions
package com.hascode.tutorial.entity;
 
import java.util.HashSet;
import java.util.Set;
 
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;
 
@NodeEntity
public class Role {
    private Long id;
 
    private String name;
 
    @Relationship(direction = Relationship.OUTGOING, type = "HAS_PERMISSION")
    private Set<Permission> permissions = new HashSet<>();
 
    public Role(String name) {
        this.name = name;
    }
 
    // hashCode, equals, toString, getter/setter, no-arg constructor ommitted..
}
Permission

Our permission entity simply has a name and no other attribute besides its primary key here:

package com.hascode.tutorial.entity;
 
import org.neo4j.ogm.annotation.NodeEntity;
 
@NodeEntity
public class Permission {
    private Long id;
 
    private String name;
 
    public Permission(String name) {
        this.name = name;
    }
 
    // hashCode, equals, toString, getter/setter, no-arg constructor ommitted..
}

Application

Now this is our permission application, executing the following steps:

  • create a new session-factory and a session
  • create multiple permissions (read, write, delete)
  • create multiple roles (administrator, editor, guest)
  • create multiple groups (group1, group2, group3)
  • create multiple users (Fred, Sally, Lisa)
  • assign users, groups, roles and permissions
  • persist all entities to the graph database
  • query and print each user’s permissions using a Cypher query
package com.hascode.tutorial.boundary;
 
import java.util.Collections;
 
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
 
import com.hascode.tutorial.entity.Group;
import com.hascode.tutorial.entity.Permission;
import com.hascode.tutorial.entity.Role;
import com.hascode.tutorial.entity.User;
 
public class PermissionExample {
    public static void main(String[] args) {
 
        SessionFactory sessionFactory = new SessionFactory("com.hascode");
        final Session session = sessionFactory.openSession();
 
        Permission canRead = new Permission("permission-read");
        Permission canWrite = new Permission("permission-write");
        Permission canDelete = new Permission("permission-delete");
 
        Role adminRole = new Role("administrator");
        Role editorRole = new Role("editor");
        Role guestRole = new Role("guest");
 
        Group group1 = new Group("group1");
        Group group2 = new Group("group2");
        Group group3 = new Group("group3");
 
        User fred = new User("Fred");
        User sally = new User("Sally");
        User lisa = new User("Lisa");
 
        adminRole.addPermission(canRead);
        adminRole.addPermission(canWrite);
        adminRole.addPermission(canDelete);
 
        editorRole.addPermission(canRead);
        editorRole.addPermission(canWrite);
 
        guestRole.addPermission(canRead);
 
        group1.addPermission(canRead);
        group1.addPermission(canWrite);
        group2.addPermission(canRead);
        group3.addPermission(canRead);
 
        group1.addRole(editorRole);
        group2.addRole(adminRole);
 
        fred.addToGroup(group1);
        sally.addToGroup(group2);
        lisa.addToGroup(group3);
 
        session.save(fred);
        session.save(sally);
        session.save(lisa);
 
        fetchPermissionFor(session, "Fred");
        fetchPermissionFor(session, "Sally");
        fetchPermissionFor(session, "Lisa");
 
    }
 
    private static void fetchPermissionFor(final Session session, String name) {
        System.out.printf("fetching permissions for %s: \n", name);
        String cypherQuery = String.format("MATCH (u:User{name:'%s'})-[*]->(p:Permission) RETURN p", name);
        session.query(Permission.class, cypherQuery, Collections.emptyMap()).forEach(System.out::println);
    }
 
}

Cypher Query for Permission Search

The following Cypher query searches for a users permissions:

MATCH (u:USER{name:"Lisa"})-[*]->(p:Permission)
RETURN p

Running the Example

We’re now ready to run our application using our IDE of choice or using the command-line-interface with Gradle and our special Gradle task “execute” like this:

$ gradle -PmainClass=com.hascode.tutorial.boundary.PermissionExample execute                                                    1 ↵
fetching permissions for Fred:
Permission [id=3, name=permission-read]
fetching permissions for Sally:
Permission [id=3, name=permission-read]
fetching permissions for Lisa:
Permission [id=3, name=permission-read]
 
BUILD SUCCESSFUL
 
Total time: 6.749 secs

Tutorial Sources

Please feel free to download the tutorial sources from my Bitbucket repository, fork it there or clone it using Git:

git clone https://bitbucket.org/hascode/neo4j-ogm-tutorial.git

Resources

Other Neo4j Tutorials

Please feel free to take a look at the other Neo4j tutorials of mine:

Graph Visualization with Gephi

Gephi is a nice tool to explore and visualize graph information.

It can be obtained for free from the project’s website here.

Here is a short demonstration using the data input from the train timetable example:

Node Editor

Gephi Node Editor

Gephi Node Editor

Edge Editor

Gephi Edge Editor

Gephi Edge Editor

Graph Visualization

Gephi rendering a Graph

Gephi rendering a Graph

There is a nice introduction written by Brian Sarnacki “The complete n00b’s Guide ot Gephi“.

Tags: , , , , , , , , , , , , , , ,

2 Responses to “Object Graph Mapping by Example with Neo4j OGM and Java”

  1. Anonymous Says:

    Hi Micha,

    Thanks for the simple yet efficient tutorial !
    I found however that the trainStation example didn’t create any nodes, which was solved by adding a commit of the transaction.

    Kind regards,
    A.D

  2. Micha Kops Says:

    Hi A.D.,
    thanks a lot for your remark!

    Kind regards,

    Micha

Search
Categories