Object Models and Spring Data Neo4j 4

· 10 min read

Drawing a graph on a whiteboard is easy and fun! Translating that graph into an object model can sometimes result in questions such as “do I have to define relationships in both participating node entities?” or “which end of the relationship should I save?”.

Your object model is key when using an object graph mapper such as Neo4j OGM. The Neo4j OGM library is the magic behind Spring Data Neo4j 4 so this article applies to both Neo4j OGM and SDN 4.

We’ll be using the ubiquitous movies domain to explain some common models.

Bidirectional Navigation

The simplest object model is also the one that represents your graph best- when you can navigate between entities(nodes) connected by relationships in either direction.

Here, we have an Actor acting in a Movie.

Graph Model

In the graph, you can navigate between the actor and movie starting at any end of the ACTED_IN relationship. We’re going to define our object model to represent this.

The Actor node entity contains an outgoing relationship to a set of Movies.

@NodeEntity(label = "Actor")
public class Actor {

	@GraphId
	private Long id;
	private String name;

	@Relationship(type = "ACTED_IN")
	private Set<Movie> actedIn = new HashSet<>();

	public Actor() {
	}

	public Actor(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void actedIn(Movie movie) {
		actedIn.add(movie);
		movie.getActors().add(this);
	}
}

The Movie node entity contains the same relationship, albeit INCOMING to the set of Actors.

@NodeEntity(label = "Movie")
public class Movie {

	@GraphId
	private Long id;
	private String title;

	@Relationship(type = "ACTED_IN", direction = "INCOMING")
	private Set<Actor> actors = new HashSet<>();

	public Movie() {
	}

	public Movie(String title) {
		this.title = title;
	}

    @Relationship(type = "ACTED_IN", direction = "INCOMING")
	public Set<Actor> getActors() {
		return actors;
	}

}

Notice how the Actor entity does not rely on a setter but on a behavioural actedIn(Movie). This ensures that the model is consistent when we relate an actor and movie. Not only do we add the movie to the actor, but we also add the actor to the movie.

Now we can start at the Actor and follow the reference to all movies he or she has acted in. We can also start at the Movie and navigate to all actors that played a role in it.

This means we can save either the actor or the movie with the same results. Remember that when persisting an entity, the default depth is -1, which means that all modified objects reachable from the root object being persisted, will be saved to the graph.

If we choose to persist the Actor, then the Movie will be persisted as well, even though it has not been explicitly saved, because it is reachable via the actedIn reference.

Actor daniel  = new Actor("Daniel Radcliffe");
Movie goblet = new Movie("Harry Potter and the Goblet of Fire");
daniel.actedIn(goblet);
actorRepository.save(daniel);

Persisting the Movie will likewise persist the Actor, reachable via the actors reference.

Actor daniel  = new Actor("Daniel Radcliffe");
Movie goblet = new Movie("Harry Potter and the Goblet of Fire");
daniel.actedIn(goblet);
movieRepository.save(goblet);

One Direction

Let’s add Users into the mix.

Graph Model

Users watch movies and we want to know which movies they’ve watched.

@NodeEntity(label = "User")
public class User {

	@GraphId
	private Long id;
	private String name;

	@Relationship(type = "WATCHED", direction = "OUTGOING")
	private Set<Movie> moviesWatched = new HashSet<>();

	public User() {
	}

	public User(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public Set<Movie> getMoviesWatched() {
		return moviesWatched;
	}

	public void watched(Movie movie) {
		moviesWatched.add(movie);
	}
}

It’s not desirable to load all Users that have watched a Movie however, so we’re going to leave the Movie entity exactly as it was, with no reference to Users. In effect, a Movie in the object model is unaware of any Users watching it.

We can go ahead and persist a User, which will in turn persist any new or modified Movies, including any new or modified Actors reachable via the Movie.

Actor daniel  = new Actor("Daniel Radcliffe");
Movie goblet = new Movie("Harry Potter and the Goblet of Fire");
Movie phoenix = new Movie("Harry Potter and the Order of the Phoenix");
daniel.actedIn(goblet);
daniel.actedIn(phoenix);

User luanne = new User("Luanne");
luanne.watched(goblet);
luanne.watched(phoenix);

userRepository.save(luanne);

However, persisting a Movie will not persist any Users because when saving the Movie, the OGM is unable to walk to a User from it.

Awards

On to our last example! This one involves the use of a relationship entity, used when we have to model properties on relationships. We’ll introduce a new relationship AWARD into the model, and have two properties on this relationship between an Actor and a Movie.

Graph Model

The relationship entity looks like this:

@RelationshipEntity(type = "AWARD")
public class Award {

	@GraphId
	private Long id;
	@StartNode Actor actor;
	@EndNode Movie movie;

	private String award;
	private int year;

	public Award() {
	}

	public Award(Actor actor, Movie movie, String award, int year) {
		this.actor = actor;
		this.movie = movie;
		this.award = award;
		this.year = year;

		this.actor.getAwards().add(this);
		this.movie.getAwards().add(this);
	}
}

We’ll add a list of Awards to both the Actor and the Movie:

@NodeEntity(label = "Actor")
public class Actor {

	@GraphId
	private Long id;
	private String name;

	@Relationship(type = "ACTED_IN")
	private Set<Movie> actedIn = new HashSet<>();

	@Relationship(type = "AWARD")
	private List<Award> awards = new ArrayList<>();

...
}

@NodeEntity(label = "Movie")
public class Movie {

	@GraphId
	private Long id;
	private String title;

	@Relationship(type = "ACTED_IN", direction = "INCOMING")
	private Set<Actor> actors = new HashSet<>();

	@Relationship(type = "AWARD", direction = "INCOMING")
	private List<Award> awards = new ArrayList<>();

...
}

Notice how we keep everything in sync- when an Award is created, it is also added to the Actor and the Movie. Now we can save the relationship entity directly, and it will persist the the actor, movie and any entities reachable from them.

Actor daniel  = new Actor("Daniel Radcliffe");
Movie goblet = new Movie("Harry Potter and the Goblet of Fire");
Movie phoenix = new Movie("Harry Potter and the Order of the Phoenix");
daniel.actedIn(goblet);
daniel.actedIn(phoenix);

Award national = new Award(daniel,phoenix,"National Movie Awards, UK",2007);
awardRepository.save(national);

We could have also saved the Actor or the Movie and the result would be the same.

What if we did not define awards on the Movie entity? We would still be able to save the Award along with the movie and actor, but saving the Movie would obviously not save any awards with it.

Keeping it together

The OGM does not keep track of how entities have changed in relation to one another. If you modify your graph but not your objects in the current session, then you would have a disconnected model and things may not behave as you would expect them to.

For example, we first persist an actor

Actor daniel  = new Actor("Daniel Radcliffe");
Movie goblet = new Movie("Harry Potter and the Goblet of Fire");
daniel.actedIn(goblet);
actorRepository.save(daniel);

and then delete the movie directly with movieRepository.delete(gobletId).

If you do not either reload the actor before persisting it again, or explicitly remove the movie from the actor object, saving the actor may establish a relationship to another Movie which reuses the ID of the movie just deleted.

So, remember to

  • Keep your sessions small enough. Ideally the scope of a Session would correspond to a unit of work. Loading entities into the Session before you update them is always a good idea.
  • Avoid anaemic domain models. Favour behavioral methods to set both sides of a relationship especially when you want bidirectional navigation.
  • Think of your object model as you’d think of your graph and things should map just fine!

Luanne Misquitta

Engineering | Neo4j certification

Luanne Misquitta is an engineering leader with over 20 years of experience in start-ups and enterprises, both consulting and product oriented. She is widely recognised as one of the world's most experienced Neo4j consultants, having successfully implemented numerous projects in the field. Luanne has a track record of growing customer-focused, high-performing engineering teams and believes in lean principles driving engineering excellence.