IMDB The Domain
From NeoWiki
We start out by looking at the domain, as this part is crucial in order to get everything else in place.
Contents |
[edit] Domain concepts
We are going to use the core data from IMDB; that is Actors and Movies. These concepts are tied together through the Role concept. So to begin with, this is our domain:
Going more into detail this is what we want to capture:
- Actors act in Movies playing Roles.
- Actors have a name.
- Movies have a title and a release year.
- Roles sometimes have a name, sometimes don't.
What kind of operations do we want to perform on our domain objects, other than common get/set operations?
- Get the Movies where an Actor appeared.
- Get the Role an Actor played in a Movie.
- Get the Actors of a Movie.
- Get Movie and Actor from a Role.
So what we want to achieve is a node space layout that fits the domain described above. To translate this into neo4j terms, it's all about:
- Nodes
- Relationships (including RelationshipTypes)
- Properties (on both nodes and relationships)
Take a look at the neo4j API when needed to get a grip on how it works.
To begin with, we have created corresponding interfaces for Actor, Movie and Role. These should be pretty obvious. This is what we have so far:
Note: Movie has no dependency on Role, as there
is no need for a Movie::getRole(Actor) method.
We can always use Actor::getRole(Movie) instead.
[edit] Implementing the domain
Our next step is to bring the design into reality by going from the domain concepts to the node space representation of them as directly as possible.
[edit] Entities in the node space
When sketching the domain entities and how they relate to each other it is obvious that the easiest way to do this is to use nodes for the actors and movies, and relationships for the roles. And here, easy is a good thing. A simple sketch could look like this:
This is then what we need to implement to get our domain objects rolling:
As seen from the diagram, ActorImpl and MovieImpl wraps a Node,
while RoleImpl wraps a Relationship.
We have choosen to represent the roles with an ACTS_IN relationship type.
This is the language of our node space:
The Actor named "Keanu Reeves" ACTS_IN the Movie titled "The Matrix",
and he ACTS_IN the role named "Neo". The next iteration of the sketch then goes like this:
There are also the IMDB relationship type, which we will come back to further on.
We should make the business rules of our domain explicitly clear at this point:
- An
Actormust have aname - The
nameof anActoris unique (this is so in the IMDB dataset) - A
Moviemust have atitleand ayear - The
titleof aMovieis unique (this is so in the IMDB dataset) - A
Rolemust be connected to oneActorand oneMovie - A
Rolemay have a name (but not all roles have this)
[edit] The Delegator pattern
Using the delegator pattern, every domain entity
is a lightweight wrapper around a neo4j primitive
(that is a Node or a Relationship).
The neo4j primitives are injected into the domain entities, and the entity objects will delegate most of the work to the underlying primitive, keeping no state except for a reference to the primitive. This means that the entity objects can be created rather freely!
Invoking an Actor instance would then look like this:
Actor actor = new ActorImpl( underlyingNode );
One good practice we shouldn't forget about is to override equals/hashCode, delegating the work to the underlying primitive.
We will start out by having a closer look at the implementation
of the Actor interface in the ActorImpl class.
[edit] Implementation of Actor
The single actual attribute we need to have in the class is the underlyingNode.
The operations needed are delegated to this Node instance in one way or another.
There's no need to perform any explicit store/fetch operations to/from the node space,
as this is handled by the transaction.
The underlying node that holds all data for this Actor is injected in the
constructor of ActorImpl.
To make sure we are consistent when using property names, constants are used to
define them.
class ActorImpl implements Actor
{
private static final String NAME_PROPERTY = "name";
private final Node underlyingNode;
ActorImpl( final Node node )
{
this.underlyingNode = node;
}
We want other classes in the domain layer to have access to the underlaying node as well. Adding this code will take care of that:
Node getUnderlyingNode()
{
return this.underlyingNode;
}
This is how we get and set a simple property:
public final String getName()
{
return (String) underlyingNode.getProperty( NAME_PROPERTY );
}
public void setName( final String name )
{
underlyingNode.setProperty( NAME_PROPERTY, name );
}
Next is to find out which movies an actor has a role in. What we do here is to fetch the "acts in" relationships from the underlying node and iterate over them. For every node we get, we instantiate a Movie object using this node.
public Iterable<Movie> getMovies()
{
final List<Movie> movies = new LinkedList<Movie>();
for ( Relationship rel : underlyingNode.getRelationships( RelTypes.ACTS_IN, Direction.OUTGOING ) )
{
movies.add( new MovieImpl( rel.getEndNode() ) );
}
return movies;
}
Note that we could add relationships of other types to the actor
nodes without interfering with fetching the ACTS_IN relationships.
Our next task is to get information about the role the actor had in a specific movie.
We start out by getting the underlying node of the Movie object.
Then we discover the ACTS_IN relationships of the actor,
If we find a relationship that connects to the correct movie node,
we return a Role object instantiated from that relationship.
If no such movie is found, null is returned.
public Role getRole( final Movie inMovie )
{
final Node movieNode = ((MovieImpl) inMovie).getUnderlyingNode();
for ( Relationship rel : underlyingNode.getRelationships( RelTypes.ACTS_IN, Direction.OUTGOING ) )
{
if ( rel.getEndNode().equals( movieNode ) )
{
return new RoleImpl( rel );
}
}
return null;
}
In this case, we will also show how to override equals()/hashCode():
@Override
public boolean equals( final Object otherActor )
{
if ( otherActor instanceof ActorImpl )
{
return this.underlyingNode.equals( ((ActorImpl) otherActor).getUnderlyingNode() );
}
return false;
}
@Override
public int hashCode()
{
return this.underlyingNode.hashCode();
}
As you see from the code, the getUnderlyingNode() method is necessary to
be able to override equals().
[edit] Implementation of Movie
The MovieImpl class starts in the same way as the ActorImpl class:
class MovieImpl implements Movie
{
private static final String TITLE_PROPERTY = "title";
private static final String YEAR_PROPERTY = "year";
private final Node underlyingNode;
MovieImpl( final Node node )
{
this.underlyingNode = node;
}
Node getUnderlyingNode()
{
return this.underlyingNode;
}
This time we are handling two properties, one String and one int type property:
public String getTitle()
{
return (String) underlyingNode.getProperty( TITLE_PROPERTY );
}
public void setTitle( final String title )
{
underlyingNode.setProperty( TITLE_PROPERTY, title );
}
public int getYear()
{
return (Integer) underlyingNode.getProperty( YEAR_PROPERTY );
}
public void setYear( final int year )
{
underlyingNode.setProperty( YEAR_PROPERTY, year );
}
Getting the actors of a movie is very similar to getting
the movies of an actor. The notable difference is that
the ACTS_IN relationships are followed in the
opposite direction. We have to look for incoming relationships
this time. Quite logical: the movie didn't "act in" the actor!
public Iterable<Actor> getActors()
{
final List<Actor> actors = new LinkedList<Actor>();
for ( Relationship rel : underlyingNode.getRelationships( RelTypes.ACTS_IN, Direction.INCOMING ) )
{
actors.add( new ActorImpl( rel.getStartNode() ) );
}
return actors;
}
Overriding equals()/hashCode() is identical, so we
don't repeat it here.
[edit] Implementation of Role
The RoleImpl is quite different from the previous classes, as
it has a relationships as its base, not a node.
This makes the start look like this:
class RoleImpl implements Role
{
private static final String ROLE_PROPERTY = "role";
private final Relationship underlyingRel;
RoleImpl( final Relationship rel )
{
this.underlyingRel = rel;
}
Relationship getUnderlyingRelationship()
{
return this.underlyingRel;
}
This class should also implement methods to get the
Actor and Movie it applies to.
As we know that the relationships points from the
actor to the movie, this is an easy task:
public Actor getActor()
{
return new ActorImpl( underlyingRel.getStartNode() );
}
public Movie getMovie()
{
return new MovieImpl( underlyingRel.getEndNode() );
}
Getting the role name will differ, while we can't be sure that
there is such a name. This is part of the domain logic.
To handle this we add a test using hasProperty()
and return null in case there is no such property set.
public String getName()
{
if ( underlyingRel.hasProperty( ROLE_PROPERTY ) )
{
return (String) underlyingRel.getProperty( ROLE_PROPERTY );
}
return null;
}
public void setName( String name )
{
underlyingRel.setProperty( ROLE_PROPERTY, name );
}
The override for equals() differs from the previous
classes in that it uses an underlying relationship, not a node:
public boolean equals( Object otherRole )
{
if ( otherRole instanceof RoleImpl )
{
return this.underlyingRel.equals( ((RoleImpl) otherRole).getUnderlyingRelationship() );
}
return false;
}
[edit] Relationship types
Part of the implementation is the RelationshipTypes we want to use. It looks like this:
public enum RelTypes implements RelationshipType
{
ACTS_IN, IMDB
}
So far we have only used the ACTS_IN relationship type.
The IMDB relationship will connect the reference nodeone to
one specific node of the node space, namely the "Kevin Bacon node".
With this, we have essentially finished the domain layer.
What's still missing is the code to create new actors, movies and the connecting roles
in our node space. This is part of the ImdbService, which is our next topic.
[edit] Node space layout
A movie has a title and a year, and will be represented like this in the node space:
In this case, the Matrix node is selected (yellow color) and it's properties are this visible in the Properties view. You can see that it has an id property (and so does every node).
An actor has a name, and of course an id.
The relationship (black arrow in image) from an actor to a movie
has an role property, an id and a relationship type
(ACTS_IN in our case).
Note: If you are playing around with Neoclipse, remember that you can't connect to the node space files from two separate neo4j engines at the same time!
Next part: IMDB Domain Services Index page: overview









