Object Models and Spring Data Neo4j 4

September 3, 2015 · 6 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")publicclassActor{@GraphIdprivateLongidprivateStringname@Relationship(type="ACTED_IN")privateSet<Movie>actedIn=newHashSet<>();publicActor(){}publicActor(Stringname){this.name=name}publicStringgetName(){returnname}publicvoidactedIn(Moviemovie){actedIn.add(moviemovie.getActors().add(this}}

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

@NodeEntity(label="Movie")publicclassMovie{@GraphIdprivateLongidprivateStringtitle@Relationship(type="ACTED_IN",direction="INCOMING")privateSet<Actor>actors=newHashSet<>();publicMovie(){}publicMovie(Stringtitle){this.title=title}@Relationship(type="ACTED_IN",direction="INCOMING")publicSet<Actor>getActors(){returnactors}}

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.

Actordaniel=newActor("Daniel Radcliffe"Moviegoblet=newMovie("Harry Potter and the Goblet of Fire"daniel.actedIn(gobletactorRepository.save(daniel

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

Actordaniel=newActor("Daniel Radcliffe"Moviegoblet=newMovie("Harry Potter and the Goblet of Fire"daniel.actedIn(gobletmovieRepository.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")publicclassUser{@GraphIdprivateLongidprivateStringname@Relationship(type="WATCHED",direction="OUTGOING")privateSet<Movie>moviesWatched=newHashSet<>();publicUser(){}publicUser(Stringname){this.name=name}publicStringgetName(){returnname}publicSet<Movie>getMoviesWatched(){returnmoviesWatched}publicvoidwatched(Moviemovie){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.

Actordaniel=newActor("Daniel Radcliffe"Moviegoblet=newMovie("Harry Potter and the Goblet of Fire"Moviephoenix=newMovie("Harry Potter and the Order of the Phoenix"daniel.actedIn(gobletdaniel.actedIn(phoenixUserluanne=newUser("Luanne"luanne.watched(gobletluanne.watched(phoenixuserRepository.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")publicclassAward{@GraphIdprivateLongid@StartNodeActoractor@EndNodeMoviemovieprivateStringawardprivateintyearpublicAward(){}publicAward(Actoractor,Moviemovie,Stringaward,intyear){this.actor=actorthis.movie=moviethis.award=awardthis.year=yearthis.actor.getAwards().add(thisthis.movie.getAwards().add(this}}

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

@NodeEntity(label="Actor")publicclassActor{@GraphIdprivateLongidprivateStringname@Relationship(type="ACTED_IN")privateSet<Movie>actedIn=newHashSet<>();@Relationship(type="AWARD")privateList<Award>awards=newArrayList<>();...}@NodeEntity(label="Movie")publicclassMovie{@GraphIdprivateLongidprivateStringtitle@Relationship(type="ACTED_IN",direction="INCOMING")privateSet<Actor>actors=newHashSet<>();@Relationship(type="AWARD",direction="INCOMING")privateList<Award>awards=newArrayList<>();...}

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.

Actordaniel=newActor("Daniel Radcliffe"Moviegoblet=newMovie("Harry Potter and the Goblet of Fire"Moviephoenix=newMovie("Harry Potter and the Order of the Phoenix"daniel.actedIn(gobletdaniel.actedIn(phoenixAwardnational=newAward(daniel,phoenix,"National Movie Awards, UK",2007awardRepository.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

Actordaniel=newActor("Daniel Radcliffe"Moviegoblet=newMovie("Harry Potter and the Goblet of Fire"daniel.actedIn(gobletactorRepository.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!

Meet the authors