IMDB Domain Services
From NeoWiki
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:
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:
- create a new node in the node space
- wrap the node inside our implementation of
Actor - set the
nameusing theActorimplementation - add the node to the index (note: not the entity object!)
- 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:
- using our simple search engine
- 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:
- try to get the node from the raw index service directly
- if not successful, use the search engine instead
- make sure to return an
Actorobject
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


