This document describes the Neo4j datastore for eXtended Objects.
Introduction
As a graph database Neo4j provides very powerful capabilities to store and query highly interconnected data structures consisting of nodes and relationships between them. With release 2.0 the concept of labels has been introduced. One or more labels can be added to a single node:
create
(a:Person:Actor)
set
a.name="Harrison Ford"
Using labels it is possible to write comprehensive queries using Cypher:
match
(a:Person)
where
a.name="Harrison Ford"
return
a.name;
If a node has a label it can be assumed that it represents some type of data which requires the presence of specific properties and relationships (e.g. property "name" for persons, "ACTED_IN" relations to movies). This implies that a Neo4j label can be represented as a Java interface and vice versa.
@Label("Person") // The value "Person" can be omitted, in this case the class name is used
public interface Person {
String getName();
void setName();
}
Maven Dependencies
The Neo4j datastore for eXtended Objects is available from Maven Central and can be specified as dependency in pom.xml files:
<project ...>
...
<properties>
<com.buschmais.xo_version>0.4.0</com.buschmais.xo_version>
</properties>
<dependencies>
<dependency>
<groupId>com.buschmais.xo</groupId>
<artifactId>xo.neo4j</artifactId>
<version>${com.buschmais.xo_version}</version>
</dependency>
</dependencies>
...
</project>
Bootstrapping
For a XOManagerFactory to be constructed a so called XO unit must be defined. There are two ways:
-
Using a descriptor META-INF/xo.xml
-
By using an instance of the class "com.buschmais.xo.XOUnit"
XML Descriptor
An XO descriptor is a XML file located as classpath resource under "/META-INF/xo.xml" and defines one or more XO units. Each must be uniquely identified by a name. This is similar to the persistence unit approach defined by the Java Persistence API (JPA). The following snippet shows a minimum setup:
<v1:xo version="1.0" xmlns:v1="http://buschmais.com/xo/schema/v1.0">
<xo-unit name="movies">
<url>file://target/movies</url>
<provider>com.buschmais.xo.neo4j.api.Neo4jXOProvider</provider>
<types>
<type>com.buschmais.xo.example.movies.composite.Movie</type>
<type>com.buschmais.xo.example.movies.composite.Person</type>
<type>com.buschmais.xo.example.movies.composite.Actor</type>
<type>com.buschmais.xo.example.movies.composite.Directory</type>
</types>
</xo-unit>
</v1:xo>
- url
-
The URL to pass to the Neo4j datastore. The following protocols are currently supported
-
"file:///C:/neo4j/movies": embedded local database using the specified directory as location for the Neo4j database
-
"http://localhost:7474/db/data": remote database over REST
-
"memory:///": non-persistent in-memory database
-
- provider
-
The class name of the datastore provider, for Neo4j com.buschmais.xo.neo4j.api.Neo4jXOProvider
- types
-
A list of all persistent interface types representing labels or relations
An XOManagerFactory instance can now be obtained as demonstrated in the following snippet:
public class Main {
public static void main(String[] args) {
XOManagerFactory xmf = XO.createXOManagerFactory("movies");
...
xmf.close();
}
}
XOUnit And XOUnitBuilder
It is also possible to create a XOManagerFactory using an instance of the class com.buschmais.xo.api.XOUnit:
public class Main {
public static void main(String[] args) {
XOUnit xoUnit = XOUnitBuilder.create(
"file://target/movies", // datastore url
Neo4jXOProvider.class, // datastore provider
Movie.class, Person.class, Actor.class, Directory.class // persistent interface types
).create();
XOManagerFactory xmf = XO.createXOManagerFactory(xoUnit);
...
xmf.close();
}
}
Note: The class XOUnitBuilder provides a fluent interface for the parameters which may be specified for an XO unit.
Mapping Persistent Types
The Neo4j database provides the following native datastore concepts:
- Node
-
An entity, e.g. a Person, Movie, etc. A node might have labels and properties.
- Relationship
-
A directed relation between two nodes, might have properties. The lifecycle of relation depends on the lifecycle of the nodes it connects.
The eXtended Objects datastore for Neo4j allows mapping of all these concepts to Java interfaces.
Nodes
Labeled Types
Neo4j allows adding one or more labels to a node. These labels are used by eXtended Objects to identify the corresponding Java type(s) a node is representing. Thus for each label that shall be used by the application a corresponding interface type must be created which is annotated with @Label.
@Label
public interface Person {
String getName();
void String setName();
}
The name of the label defaults to the name of the interface, in this case Person. A specific value can be enforced by adding a value to the @Label annotation.
It can also be seen that a label usually enforces the presence of specific properties (or relations) on a node. The name of a property - starting with a lower case letter - is used to store its value in the database, this can be overwritten using @Property. The following example demonstrates explicit mappings for a label and a property:
@Label("MyPerson")
public interface Person {
@Property("myName")
String getName();
void String setName();
}
The mapping of relations will be covered later.
Inheritance Of Labels
A labeled type can extend from one or more labeled types.
@Label
public interface Actor extends Person {
}
In this case a node created using the type Actor would be labeled with both Person and Actor. This way of combining types is referred to as static composition.
Template Types
There might be situations where the same properties or relations shall be re-used between various labels. In this case template types can be used, these are just interfaces specifying properties and relations which shall be shared. The following example demonstrates how the property name of the labeled type Person is extracted to a template type:
public interface Named {
String getName();
void setName(String name);
}
@Label
public interface Person extends Named {
}
Relations
Unidirectional Relations
A node can directly reference other nodes using relation properties. A property of a labeled type or template type is treated as such if it references another labeled type or a collection thereof.
@Label
public interface Movie {
String getTitle();
void setTitle();
}
@Label
public interface Actor extends Person {
List<Movie> getActedIn();
}
If no further mapping information is provided an outgoing unidirectional relation using the fully capitalized name of the property is assumed. The name may be specified using the @Relation annotation with the desired value. Furthermore using one of the annotations @Outgoing or @Incoming the direction of the relation can be specified.
@Label
public interface Actor extends Person {
@Relation("ACTED_IN")
@Outgoing
List<Movie> getActedIn();
}
Note on multi-valued relations (i.e. collections):
-
Only the following types are supported: java.util.Collection, java.util.List or java.util.Set.
-
It is recommend to only specify the getter method of the property, as add or remove operations can be performed using the corresponding collection methods
-
The provided java.util.Set implementation ensures uniqueness of the relation to the referenced node, if this is not necessary java.util.List should be prefered for faster add-operations.
Bidirectional Qualified Relations
Relations in many case shall be accessible from both directions. One possible way is to use two independent unidirectional relations which map to the same relation type; one of them annotated with @Outgoing, the other with @Incoming. There are some problems with this approach:
-
it is not explicitly visible that the two relation properties are mapped to the same type
-
renaming of the type or of one the properties might break the mapping
The recommended way is to use an annotation which qualifies the relation and holds the mapping information at a single point:
@Relation
@Retention(RUNTIME)
public @interface ActedIn {
}
@Label
public interface Actor extends Person {
@ActedIn
@Outgoing
List<Movie> getActedIn();
}
@Label
public interface Movie {
String getTitle();
void setTitle();
@ActedIn
@Incoming
List<Actors> getActors();
}
Typed Relations With Properties
If a relation between two nodes shall have properties a dedicated type must be declared. It must contain two properties returning the types of referenced types which are annotated with @Incoming and @Outgoing:
@Relation
public interface Directed {
@Outgoing
Director getDirector();
@Incoming
Movie getMovie();
Calendar getFrom();
void setFrom(Calendar from);
Calendar getUntil();
void setUntil(Calender until);
}
@Label
public interface Director extends Person {
List<Directed> getDirected();
}
@Label
public interface Movie {
String getTitle();
void setTitle();
List<Directed> getDirected();
...
}
Note: If the typed relation references the same labeled type at both ends then the according properties of the latter must also be annotated with @Outgoing and @Incoming:
@Relation
public interface References {
@Outgoing
Movie getReferencing();
@Incoming
Movie getReferenced();
int getMinute();
void setMinute(int minute);
int getSecond()
void setSecond(int second);
}
@Label
public interface Movie {
@Outgoing
List<References> getReferenced();
@Incoming
List<References> getReferencedBy();
...
}
Typed relations may also be constructed using Template Types, i.e. types which define commonly used Properties.
Dynamic Properties
Labeled types or relation types may also define methods which execute a query on invocation and return the result:
@Label
public interface Movie {
@ResultOf
@Cypher("match (m:Movie) where m.title={title} return m");
Result<Movie> getMoviesByTitle(@Parameter("title") String title);
@ResultOf
@Cypher("match (a:Actor)-[:ACTED_IN]->(m:Movie) where id(m)={this} return count(a)");
Long getActorCount();
...
}
Transient Properties
Properties of entities or relations can be declared as transient, i.e. they may be used at runtime but will not be stored in the database:
@Label
public interface Person {
@Transient
String getName();
void setName();
}
User defined methods
It can be useful to provide a custom implementation of a method which has direct access to the underlying datatypes. This can be achieved using @ImplementedBy.
@Label
public interface Person {
@ImplementedBy(SetNameMethod.class)
String setName(String firstName, String lastName);
}
public class SetNameMethod implements ProxyMethod<Node> {
@Override
public Object invoke(Node node, Object instance, Object[] args) {
String firstName = (String) args[0];
String lastName = (String) args[1];
String fullName = firstName + " " + lastName;
node.setProperty("name", fullName);
return fullName;
}
}