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(String name);
}
Maven Dependencies
The Neo4j datastore for eXtended Objects is available from Maven Central and can be specified as dependency in pom.xml files:
<dependency>
<!-- For using an embedded Neo4j instance -->
<groupId>com.buschmais.xo</groupId>
<artifactId>xo.neo4j.embedded</artifactId>
<version>0.8.0</version>
</dependency>
<dependency>
<!-- For using a remote Neo4j instance -->
<groupId>com.buschmais.xo</groupId>
<artifactId>xo.neo4j.remote</artifactId>
<version>0.8.0</version>
</dependency>
<dependency>
<!-- The XO API -->
<groupId>com.buschmais.xo</groupId>
<artifactId>xo.api</artifactId>
<version>0.8.0</version>
</dependency>
<dependency>
<!-- The XO runtime implementation -->
<groupId>com.buschmais.xo</groupId>
<artifactId>xo.impl</artifactId>
<version>0.8.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<!-- An SLF4j binding -->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>runtime</scope>
<version>1.7.21</version>
</dependency>
<!-- Optional dependencies for in-memory databases, i.e. URI "memory:///" -->
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-kernel</artifactId>
<type>test-jar</type>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-io</artifactId>
<type>test-jar</type>
<version>3.1.2</version>
</dependency>
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"
XOUnit via XML
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 setups
<v1:xo version="1.0" xmlns:v1="http://buschmais.com/xo/schema/v1.0">
<xo-unit name="movies">
<url>file:databases/movies</url>
<provider>com.buschmais.xo.neo4j.embedded.api.EmbeddedNeo4jXOProvider</provider>
<types>
<type>com.buschmais.xo.neo4j.doc.bootstrap.Person</type>
<type>com.buschmais.xo.neo4j.doc.bootstrap.Actor</type>
</types>
</xo-unit>
</v1:xo>
<v1:xo version="1.0" xmlns:v1="http://buschmais.com/xo/schema/v1.0">
<xo-unit name="movies">
<url>bolt://localhost:7687</url>
<provider>com.buschmais.xo.neo4j.remote.api.RemoteNeo4jXOProvider</provider>
<types>
<type>com.buschmais.xo.neo4j.doc.bootstrap.Person</type>
<type>com.buschmais.xo.neo4j.doc.bootstrap.Actor</type>
</types>
<properties>
<property name="neo4j.remote.username" value="neo4j"/>
<property name="neo4j.remote.password" value="secret"/>
<!--
<property name="neo4j.remote.encryptionLevel" value="required"/>
<property name="neo4j.remote.trust.strategy" value="trustCustomCaSignedCertificates"/>
<property name="neo4j.remote.trust.certificate" value="my.crt"/>
<property name="neo4j.remote.statement.log.level" value="info"/>
-->
</properties>
</xo-unit>
</v1:xo>
Note
|
Support for remote databases is experimental. |
- provider
-
The class name of the datastore provider
-
com.buschmais.xo.neo4j.embedded.api.EmbeddedNeo4jXOProvider
for embedded Neo4j databases -
com.buschmais.xo.neo4j.remote.api.RemoteNeo4jXOProvider
for remote Neo4j databases via bolt protocol
-
- 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
-
"memory:///": embedded non-persistent in-memory database
-
"bolt://localhost:7687": remote database
-
- types
-
A list of all persistent types representing labels, relations or repositories
An XOManagerFactory instance can now be obtained as demonstrated in the following snippet:
XOManagerFactory movies = XO.createXOManagerFactory("movies");
XOManager xoManager = movies.createXOManager();
xoManager.currentTransaction().begin();
Person person = xoManager.create(Person.class);
person.setName("Indiana Jones");
xoManager.currentTransaction().commit();
xoManager.close();
movies.close();
XOUnit via XOUnitBuilder
It is also possible to create a XOManagerFactory using an instance of the class 'com.buschmais.xo.api.XOUnit':
XOUnit xoUnit = XOUnit.builder().provider(EmbeddedNeo4jXOProvider.class).uri(new URI("file:databases/movies")).type(Person.class).type(Actor.class)
.build();
XOManagerFactory movies = XO.createXOManagerFactory(xoUnit);
XOManager xoManager = movies.createXOManager();
xoManager.currentTransaction().begin();
Person person = xoManager.create(Person.class);
person.setName("Indiana Jones");
xoManager.currentTransaction().commit();
xoManager.close();
movies.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 setName(String name);
}
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 setName(String name);
}
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<Actor> getActors();
}
Typed Relations With Properties
If a relation between two nodes shall have properties a dedicated type must be declared and registered in the XOUnit. 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();
int getYear();
void setYear(int year);
}
@Label
public interface Director extends Person {
List<Directed> getDirected();
}
@Label
public interface Movie {
String getTitle();
void setTitle();
List<Directed> getDirected();
}
The relation is created explicitly:
xoManager.currentTransaction().begin();
Director director = xoManager.create(Director.class);
Movie movie = xoManager.create(Movie.class);
Directed directed = xoManager.create(director, Directed.class, movie);
directed.setYear(2017);
xoManager.currentTransaction().commit();
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 (a:Actor)-[:ACTED_IN]->(m:Movie) where id(m)={this} return count(a)")
Long getActorCount();
@ResultOf
@Cypher("match (a:Actor)-[:ACTED_IN]->(m:Movie) where id(m)={this} and a.age={age} return count(a)")
Long getActorCountByAge(@Parameter("age") int age);
@ActedIn
@Incoming
List<Actor> getActors();
}
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(String name);
}
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', the following example uses an embedded Neo4j instance:
@Label
public interface Person {
@ImplementedBy(SetNameMethod.class)
String setName(String firstName, String lastName);
class SetNameMethod implements ProxyMethod<EmbeddedNode> {
@Override
public Object invoke(EmbeddedNode 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;
}
}
}
Repositories
eXtended Objects supports the concepts of repositories. Similar to nodes and relationships they are also represented by Java interfaces and must be registered in the XOUnit. They allow the definition of dynamic properties:
@Repository public interface PersonRepository { @ResultOf @Cypher("MATCH (p:Person) WHERE p.name={name} RETURN p") Result<Person> getPersonsByName(@Parameter("name") String name); }
A repository instance can be obtained from the XOManager:
xoManager.currentTransaction().begin(); Person person = xoManager.create(Person.class); person.setName("Indiana Jones"); PersonRepository personRepository = xoManager.getRepository(PersonRepository.class); Result<Person> personsByName = personRepository.getPersonsByName("Indiana Jones"); Person indy = personsByName.getSingleResult(); xoManager.currentTransaction().commit();
There is a predefined repository type that allows finding instances by labels.
The following class declares an indexed property name
for the label Person
:
@Label public interface Person { @Indexed String getName(); void setName(String name); }
It is possible to define a repository which inherits from TypedNeo4jRepository
and pass Person
as type argument:
@Repository public interface TypedPersonRepository extends TypedNeo4jRepository<Person> { }
This allows using the provided find
method:
xoManager.currentTransaction().begin(); Person person = xoManager.create(Person.class); person.setName("Indiana Jones"); PersonRepository personRepository = xoManager.getRepository(PersonRepository.class); Result<Person> personsByName = personRepository.getPersonsByName("Indiana Jones"); Person indy = personsByName.getSingleResult(); xoManager.currentTransaction().commit();