Our application was not yet very much fun yet, just storing movies and actors. After all, the power is in the relationships between them. Fortunately, Neo4j treats relationships as first class citizens, allowing them to be addressed individually and assigned properties. That allows for representing them as entities if needed.
Relationships without properties ("anonymous" relationships) don't require any @RelationshipEntity classes. Unfortunately we had none of those, because our relationships were richer. Therefore we went with the Role relationship between Movie and Actor. It had to be annotated with @RelationshipEntity and the @StartNode and @EndNode had to be marked. So our Role looked like this:
Example 9.1. Role class
@RelationshipEntity
class Role {
@StartNode Actor actor;
@EndNode Movie movie;
String role;
}
When writing a test for that we tried to create the relationship entity with the new
keyword, but we got an exception saying that it was not allowed. At first this surprised us, but
then we realized that a relationship entity must have a starting entity and ending entity. It
turned out that the aspect had introduced a entity.relateTo
method in the node entities.
It turned out to be exactly what we needed. We simply added a method to the Actor class, connecting
it to movies.
Example 9.2. Relating actors to movies
class Actor { ... public Role playedIn(Movie movie, String roleName) { Role role = relateTo(movie, Role.class, "ACTS_IN"); role.setRole(roleName); return role; } }
Now we wanted to find connected entities. We already had fields for the relationships in both classes. It was time to annotate them correctly. It turned out that we needed to provide the target type of the fields again, due to Java's type erasure. The Neo4j relationship type and direction were easy to figure out. The direction even defaulted to outgoing, so we only had to specify it for the movie.
Example 9.3. @RelatedTo usage
@NodeEntity class Movie { @Indexed int id; String title; int year; @RelatedTo(elementClass = Actor.class, type = "ACTS_IN", direction = Direction.INCOMING) Set<Actor> cast; } @NodeEntity class Actor { @Indexed int id; String name; @RelatedTo(elementClass = Movie.class, type = "ACTS_IN") Set<Movie> movies; public Role playedIn(Movie movie, String roleName) { Role role = relateTo(movie, Role.class, "ACTS_IN"); role.setRole(roleName); return role; } }
While reading about these relationship collections, we learned that they are actually Spring Data Neo4j-managed sets. So whenever we add or remove something from the set, it automatically gets reflected in the underlying relationships. That's neat! But this also meant we did not need to initialize the fields. That could be easy to forget.
We made sure to add a test for those, so we were assured that the collections worked as advertised.
But we still couldn't access the Role relationships. It turned out that there was a separate
annotation @RelatedToVia
for accessing the actual relationship entities. And we had
to declare the field as an Iterable<Role>, with read-only semantics. This appeared to mean
that we were not able to add new roles through the field. Adding relationship entities seemed like
it had to be done by using entity.relateTo()
. The annotation attributes were similar to
those used for @RelatedTo
. So off we went, creating our first real relationship (just kidding).
Example 9.4. @RelatedToVia usage
@NodeEntity class Movie { @Indexed int id; String title; int year; @RelatedTo(elementClass = Actor.class, type = "ACTS_IN", direction = Direction.INCOMING) Set<Actor> cast; @RelatedToVia(elementClass = Role.class, type = "ACTS_IN", direction = Direction.INCOMING) Iterable<Roles> roles; }
After watching the tests pass, we were confident that the relationship fields really mirrored the underlying relationships in the graph. We were pretty satisfied with our domain.