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.
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 Movie
s.
@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 Actor
s.
@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 User
s into the mix.
User
s 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 User
s that have watched a Movie
however, so we’re going to leave the Movie
entity exactly as it was, with no reference to User
s. In effect, a Movie
in the object model is unaware of any User
s watching it.
We can go ahead and persist a User
, which will in turn persist any new or modified Movie
s, including any new or modified Actor
s 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 User
s 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
.
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 Award
s 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 theSession
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!