IMDB Domain Services

From NeoWiki

Jump to: navigation, search

Contents

[edit] Adding services

As part of the domain layer, we have a service part that connects to the neo4j instance and other services we want to use for different tasks. This keeps our entity objects very lightweight and free from any direct dependency on the neo4j engine or the index service.

Lets first have a look at the interface that should get implemented:

public interface ImdbService
{
    Actor createActor( String name );
    Movie createMovie( String title, int year );
    Role createRole( Actor actor, Movie movie, String roleName );
    Actor getActor( String name );
    Movie getMovie( String title );
    List<?> getBaconPath( Actor actor );
    void setupReferenceRelationship();
}

What we have here is:

  • create*() methods aimed at creating domain objects
  • get*() methods to search for objects
  • getBaconPath() method is a domain-related functionality we want to use
  • setupReferenceRelationship() connects the "Kevin Bacon node" to speed up fetching it

This is the context of the ImdbServiceImpl what we are going to look closer into:

Image:Imdb.domain.service.png

We will make use of the IndexService API to create and access our indices:

public interface IndexService
{
    void index( Node node, String key, Object value );
    Node getSingleNode( String key, Object value );
    Iterable<Node> getNodes( String key, Object value );
    void removeIndex( Node node, String key, Object value );
    void setIsolation( Isolation level );
    void shutdown();
}

In this example we will only use the index() and getSingleNode() methods in the IndexService API.

Note: We will not add transactions to this class, so that consumers can decide on running one or more calls inside of a transaction.

That's all of the prerequisites, so let's get into the code.

[edit] Dependency setup

To begin with, we have to setup the dependencies to get injected by Spring Framework. This is how it looks:

class ImdbServiceImpl implements ImdbService
{
    @Autowired
    private NeoService neoService;
    @Autowired
    private IndexService indexService;
    @Autowired
    private PathFinder pathFinder;
    @Autowired
    private ImdbSearchEngine searchEngine;

This provides us with access to the neo4j engine, the index service, a path finder and our simple search engine.

[edit] Create entities

The dependencies in place, our next task is to create our domain entities in the node space.

To make sure that we always use the same name for the index keys, we define them as constants, together with some property names and stuff like that.

    private static final String TITLE_INDEX = "title";
    private static final String NAME_INDEX = "name";

Now we'll start off with the createActor() part.

What this piece of code does is:

  1. create a new node in the node space
  2. wrap the node inside our implementation of Actor
  3. set the name using the Actor implementation
  4. add the node to the index (note: not the entity object!)
  5. return the entity object
    public Actor createActor( final String name )
    {
        final Node actorNode = neoService.createNode();
        final Actor actor = new ActorImpl( actorNode );
        actor.setName( name );
        searchEngine.indexActor( actor );
        indexService.index( actorNode, NAME_INDEX, name );
        return actor;
    }

Here we index the new actor in two different ways:

  1. using our simple search engine
  2. using the index service directly

The createMovie() method is more or less identical to the actor counterpart:

    public Movie createMovie( final String title, final int year )
    {
        final Node movieNode = neoService.createNode();
        final Movie movie = new MovieImpl( movieNode );
        movie.setTitle( title );
        movie.setYear( year );
        searchEngine.indexMovie( movie );
        indexService.index( movieNode, TITLE_INDEX, title );
        return movie;
    }

Creating and storing (well, storing happens without us having to do anything about it -- persisting changes happens when the transaction is commited) a role is slightly more complicated. The following code should be self-explanatory. We check preconditions and then set up the relationship and the role name if it exists.

    public Role createRole( final Actor actor, final Movie movie, final String roleName )
    {
        if ( actor == null )
        {
            throw new IllegalArgumentException( "Null actor" );
        }
        if ( movie == null )
        {
            throw new IllegalArgumentException( "Null movie" );
        }
        final Node actorNode = ((ActorImpl) actor).getUnderlyingNode();
        final Node movieNode = ((MovieImpl) movie).getUnderlyingNode();
        final Relationship rel = actorNode.createRelationshipTo( movieNode, RelTypes.ACTS_IN );
        final Role role = new RoleImpl( rel );
        if ( roleName != null )
        {
            role.setName( roleName );
        }
        return role;
    }


[edit] Finding entities

Now when we have managed to create entities in the node space, we want to find them again. Let's start with the actors.

This is what's going on here:

  1. try to get the node from the raw index service directly
  2. if not successful, use the search engine instead
  3. make sure to return an Actor object
    public Actor getActor( final String name )
    {
        Node actorNode = indexService.getSingleNode( NAME_INDEX, name );
        if ( actorNode == null )
        {
            actorNode = searchEngine.searchActor( name );
        }
        Actor actor = null;
        if ( actorNode != null )
        {
            actor = new ActorImpl( actorNode );
        }
        return actor;
    }

The code for getMovie() is very similar. Note that we used different keys for indexing actors and movies. This way we can also be sure of what type of node we are receiving from the search without any extra checking.

    public Movie getMovie( final String title )
    {
        Node movieNode = indexService.getSingleNode( TITLE_INDEX, title );
        if ( movieNode == null )
        {
            movieNode = searchEngine.searchMovie( title );
        }
        Movie movie = null;
        if ( movieNode != null )
        {
            movie = new MovieImpl( movieNode );
        }
        return movie;
    }


[edit] Going the Bacon path

We have placed the actual path finding code in package of its own, but we still need some code to call it from this service, to be able to provide it to the domain layer consumers.

To get to the Bacon node in a convenient and fast way, we added a method that is called during the setup process. The setupReferenceRelationship() method sets up a subreference node so that we can find the Bacon node through a relationship from the reference node.

    @Transactional
    public void setupReferenceRelationship()
    {
        Node baconNode = indexService.getSingleNode( "name", "Bacon, Kevin" );
        if ( baconNode == null )
        {
            throw new NoSuchElementException( "Unable to find Kevin Bacon actor" );
        }
        Node referenceNode = neoService.getReferenceNode();
        referenceNode.createRelationshipTo( baconNode, RelTypes.IMDB );
    }

The getBaconPath() method fetches the Kevin Bacon node, and then forwards this and the actor node to the path finder implementation. At the end, the raw nodes are wrapped in Actor and Movie objects.

    public List<?> getBaconPath( final Actor actor )
    {
        final Node baconNode;
        if ( actor == null )
        {
            throw new IllegalArgumentException( "Null actor" );
        }
        try
        {
            baconNode = neoService.getReferenceNode().getSingleRelationship( RelTypes.IMDB, Direction.OUTGOING ).getEndNode();
        }
        catch ( NoSuchElementException e )
        {
            throw new NoSuchElementException( "Unable to find Kevin Bacon actor" );
        }
        final Node actorNode = ((ActorImpl) actor).getUnderlyingNode();
        final List<Node> list = pathFinder.shortestPath( actorNode, baconNode, RelTypes.ACTS_IN );
        return convertNodesToActorsAndMovies( list );
    }

The Bacon node is fetched by getting the outgoing IMDB type relationship from the reference node and fetching the end node from this relationship. If it's not set up correctly, the next() call will throw an NoSuchElementException. We just re-throw the exception with a better message.

The convertNodesToActorsAndMovies method performs the wrapping of raw nodes into domain objects. As we know that a path through our node space will always consist of alternating actor and movie nodes, we can easily construct a loop over the path transforming every item to an Actor or Movie node.

    private List<?> convertNodesToActorsAndMovies( final List<Node> list )
    {
        List<Object> actorAndMovieList = new LinkedList<Object>();
        int mod = 0;
        for ( Node node : list )
        {
            if ( mod++ % 2 == 0 )
            {
                actorAndMovieList.add( new ActorImpl( node ) );
            }
            else
            {
                actorAndMovieList.add( new MovieImpl( node ) );
            }
        }
        return actorAndMovieList;
    }


Next part: IMDB Search Engine Index page: overview

Personal tools