IMDB The Domain

Neo4j Wiki から

We start out by looking at the domain, as this part is crucial in order to get everything else in place. You will also learn a basic way to persist domain objects using Neo4j.

目次

[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:

Image:Imdb.domain.interface.simple.png


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 graph 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:

Image:Imdb.domain.interface.png

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 graph representation of them as directly as possible.

[edit] Entities in the graph

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:

Image:Imdb.domain.sketch.png


This is then what we need to implement to get our domain objects rolling:

Image:Imdb.domain.impl.png


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 graph: 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:

Image:Imdb.domain.sketch.impl.png

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 Actor must have a name
  • The name of an Actor is unique (this is so in the IMDB dataset)
  • A Movie must have a title and a year
  • The title of a Movie is unique (this is so in the IMDB dataset)
  • A Role must be connected to one Actor and one Movie
  • A Role may 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 graph, 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 use getProperty( String key, Object defaultValue ) using null as the default value. So if the role doesn't have a name, null will be returned.

    public String getName()
    {
        return ( String ) underlyingRel.getProperty( ROLE_PROPERTY, 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 node to one specific node in the graph, 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 graph. This is part of the ImdbService, which is our next topic.


[edit] Graph layout

A movie has a title and a year, and will be represented like this in the graph:

Image:Imdb.screenshot.movie.png

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.


Image:Imdb.screenshot.actor.png


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).


Image:Imdb.screenshot.role.png

Note: If you are playing around with Neoclipse, remember that you can't connect to the Neo4j db files from two separate Neo4j engines at the same time!
The important thing to remember from this part is how to create domain objects using underlying nodes and relationships. Go to the whiteboard and outline your domain; then create a corresponding node/relationship structure.

Next part: IMDB Domain Services Index page: overview

Neo4j のサイト
ツールボックス
Neo4j のサイト
ツールボックス