Object Graph Mapping by Example with Neo4j OGM and Java
July 18th, 2016 by Micha KopsWhen 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.
Contents
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.
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.
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:
- Neo4j Graph Database Tutorial: How to build a Route Planner and other Examples
- A short Overview of Neo4j Indexing Strategies
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
Edge Editor
Graph Visualization
There is a nice introduction written by Brian Sarnacki “The complete n00b’s Guide ot Gephi“.
Tags: cypher, dijkstra, edge, gephi, gradle, graph, graphdb, neo4j, neo4jogm, node, nosql, ogm, tools, vertex, vertices, visualization
July 13th, 2022 at 10:03 pm
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
September 26th, 2022 at 9:31 pm
Hi A.D.,
thanks a lot for your remark!
Kind regards,
Micha