Reference
Introduction
Neo4j-OGM is a fast object-graph mapping library for Neo4j, optimised for server-based installations utilising Cypher.
It aims to simplify development with the Neo4j graph database and like JPA, it uses annotations on simple POJO domain objects to do so.
With a focus on performance, Neo4j-OGM introduces a number of innovations, including:
-
non-reflection based classpath scanning for much faster startup times
-
variable-depth persistence to allow you to fine-tune requests according to the characteristics of your graph
-
smart object-mapping to reduce redundant requests to the database, improve latency and minimise wasted CPU cycles
-
user-definable session lifetimes, helping you to strike a balance between memory-usage and server request efficiency in your applications.
Overview
This reference documentation is broken down into sections to help the user understand specifics of how Neo4j-OGM works.
- Getting started
-
Getting started can sometimes be a chore. What versions of Neo4j-OGM do you need? Where do you get them from? What build tool should you use? Getting Started is the perfect place to well… get started!
- Configuration
-
Drivers, logging, properties, configuration via Java. How to make sense of all the options? Configuration has got you covered.
- Annotating your Domain Objects
-
To get started with your Neo4j-OGM application, you need only your domain model and the annotations provided by the library. You use annotations to mark domain objects to be reflected by nodes and relationships of the graph database. For individual fields the annotations allow you to declare how they should be processed and mapped to the graph. For property fields and references to other entities this is straightforward. Because Neo4j is a schema-free database, Neo4j-OGM uses a simple mechanism to map Java types to Neo4j nodes using labels. Relationships between entities are first class citizens in a graph database and therefore worth a section of it’s own describing their usage in Neo4j-OGM.
- Connecting to the Database
-
Managing how you connect to the database is important. Connecting to the Database has all the details on what needs to happen to get you up and running.
- Interacting with the Graph Model
-
Neo4j-OGM offers a session for interacting with the mapped entities and the Neo4j graph database. Neo4j uses transactions to guarantee the integrity of your data and Neo4j-OGM supports this fully. The implications of this are described in the transactions section. To use advanced functionality like Cypher queries, a basic understanding of the graph data model is required. The graph data model is explained in the chapter about in the introduction chapter.
- Type Conversion
-
Neo4j-OGM provides support for default and bespoke type conversions, which allow you to configure how certain data types are mapped to nodes or relationships in Neo4j. See Type Conversion for more details.
- Filtering your domain objects
-
Filters provides a simple API to append criteria to your stock
Session.loadX()
behaviour. This is covered in more detail in Filters. - Reacting to Persistence events
-
The Events mechanism allows users to register event listeners for handling persistence events related both to top-level objects being saved as well as connected objects. Event handling discusses all the aspects of working with events.
- Testing in your application
-
Sometimes you want to be able to run your tests against an in-memory version of Neo4j-OGM. Testing goes into more detail of how to set that up.
Getting Started
Versions
Consult the version table to determine which version of Neo4j-OGM to use with a particular version of Neo4j and related technologies.
Compatibility
Neo4j-OGM Version | Neo4j Version1 |
---|---|
4.0.x2 |
4.4.x6, 5.x |
3.2.x |
3.2.x, 3.3.x, 3.4.x, 3.5.x, 4.0.x2, 4.1.x2, 4.2.x2, 4.3.x2,5, 4.4.x2,5 |
3.1.x3 |
3.1.x, 3.2.x, 3.3.x, 3.4.x |
3.0.x3 |
3.1.9, 3.2.12, 3.3.4, 3.4.4 |
2.1.x4 |
2.3.9, 3.0.11, 3.1.6 |
2.0.24 |
2.3.8, 3.0.7 |
2.0.14 |
2.2.x, 2.3.x |
1 The latest supported bugfix versions.
2 These versions only support connections via Bolt.
3 These versions are no longer actively developed.
4 These versions are no longer actively developed or supported.
5 Neo4j-OGM 3.2.24+ only.
6 Technical working but not officially supported
Dependency Management
For building an application, your build automation tool needs to be configured to include the Neo4j-OGM dependencies.
Neo4j-OGM dependencies consist of neo4j-ogm-core
, together with the relevant dependency declarations on the driver you want to use.
Neo4j-OGM 4.x provides only support for the Bolt driver, but for compatibility reasons you have to declare the dependency:
-
neo4j-ogm-bolt-driver
- Uses native Bolt protocol to communicate between Neo4j-OGM and a remote Neo4j instance. -
neo4j-ogm-bolt-native-types
- Support for all of Neo4j’s property types through the Bolt protocol.
Neo4j-OGM projects can be built using Maven, Gradle or any other build system that utilises Maven’s artifact repository structure.
Maven
In the <dependencies>
section of your pom.xml
add the following:
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-ogm-core</artifactId>
<version>4.0.13</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-ogm-bolt-driver</artifactId>
<version>4.0.13</version>
<scope>runtime</scope>
</dependency>
Please also have a look at the native type system to take advantage of Neo4j-OGM’s support for native temporal and spatial types.
Configuration
Configuration method
There are several ways to supply configuration to Neo4j-OGM:
-
using a properties file
-
programmatically using Java
-
by providing an already configured Neo4j Java driver instance
These methods are described below. They are also available as code in the examples.
Using a properties file
Properties file on classpath:
ConfigurationSource props = new ClasspathConfigurationSource("my.properties");
Configuration configuration = new Configuration.Builder(props).build();
Properties file on filesystem:
ConfigurationSource props = new FileConfigurationSource("/etc/my.properties");
Configuration configuration = new Configuration.Builder(props).build();
Programmatically using Java
In cases where you are not able to provide configuration via a properties file you can configure Neo4j-OGM programmatically instead.
The Configuration
object provides a fluent API to set various configuration options.
This object then needs to be supplied to the SessionFactory
constructor in order to be configured.
By providing a Neo4j driver instance
Just configure the driver as you would do for direct access to the database, and pass the driver instance to the session factory.
This method allows the greatest flexibility and gives you access to the full range of low level configuration options.
org.neo4j.driver.Driver nativeDriver = ...;
Driver ogmDriver = new BoltDriver(nativeDriver);
new SessionFactory(ogmDriver, ...);
Driver Configuration
For configuration through properties file or configuration builder the driver is automatically inferred from given URI. Empty URI means embedded driver with impermanent database.
Bolt Driver
Note that for the URI
, if no port is specified, the default Bolt port of 7687
is used.
Otherwise, a port can be specified with bolt://neo4j:password@localhost:1234
.
Also, the bolt driver allows you to define a connection pool size, which refers to the maximum number of sessions per URL.
This property is optional and defaults to 50
.
ogm.properties | Java Configuration |
---|---|
|
|
A timeout to the database with the Bolt driver can be set by updating your Database’s neo4j.conf
.
The exact setting to change can be found here.
Credentials
If you are using the Bolt Driver you have a number of different ways to supply credentials to the Driver Configuration.
ogm.properties | Java Configuration |
---|---|
|
|
Note: Currently only Basic Authentication is supported by Neo4j-OGM. If you need to use more advanced authentication scheme, use the native driver configuration method.
Transport Layer Security (TLS/SSL)
The Bolt and HTTP drivers also allow you to connect to Neo4j over a secure channel. These rely on Transport Layer Security (aka TLS/SSL) and require the installation of a signed certificate on the server.
In certain situations (e.g. some cloud environments) it may not be possible to install a signed certificate even though you still want to use an encrypted connection.
To support this, both drivers have configuration settings allowing you to bypass certificate checking, although they differ in their implementation.
Both of these strategies leave you vulnerable to a MITM attack. You should probably not use them unless your servers are behind a secure firewall. |
Bolt
ogm.properties | Java Configuration |
---|---|
|
|
TRUST_ON_FIRST_USE
means that the Bolt Driver will trust the first connection to a host to be safe and intentional.
On subsequent connections, the driver will verify that the host is the same as on that first connection.
Bolt connection testing
In order to prevent some network problems while accessing a remote database, you may want to tell the Bolt driver to test connections from the connection pool.
This is particularly useful when there are firewalls between the application tier and the database.
You can do that with the connection liveness parameter which indicates the interval at which the connections will be tested. A value of 0 indicates that the connection will always be tested. A negative value indicates that the connection will never be tested.
ogm.properties | Java Configuration |
---|---|
|
|
Eager connection verification
OGM by default does not connect to Neo4j server on application startup.
This allows you to start the application and database independently and Neo4j will be accessed on first read/write.
To change this behaviour set the property verify.connection
(or Builder.verifyConnection(boolean)
) to true.
This settings is valid only for Bolt drivers.
Logging
Neo4j-OGM uses SLF4J to log statements. In production, you can set the log level in a file called logback.xml to be found at the root of the classpath. Please see the Logback manual for further details.
An important logger is the BoltResponse
logger.
It has multiple "sub-logger" for Neo4j notification categories that may come up when using e.g. deprecated features.
An overview can be seen in the following list.
-
org.neo4j.ogm.drivers.bolt.response.BoltResponse.performance
-
org.neo4j.ogm.drivers.bolt.response.BoltResponse.hint
-
org.neo4j.ogm.drivers.bolt.response.BoltResponse.unrecognized
-
org.neo4j.ogm.drivers.bolt.response.BoltResponse.unsupported
-
org.neo4j.ogm.drivers.bolt.response.BoltResponse.deprecation
-
org.neo4j.ogm.drivers.bolt.response.BoltResponse.generic
-
org.neo4j.ogm.drivers.bolt.response.BoltResponse.security
-
org.neo4j.ogm.drivers.bolt.response.BoltResponse.topology
You can still use the org.neo4j.ogm.drivers.bolt.response.BoltResponse
logger as the main logger and just adjust the details in some details to your needs.
Class loading precedence
In some scenarios and environments (Spring Boot’s @Async
annotated classes/methods, CompletableFuture
usage, etc.) , it is necessary to declare the used class loading precedence for Neo4j-OGM to use.
As default, it uses the current thread’s context class loader.
To change this behaviour, the OGM_CLASS_LOADER
has to be set only once for the Configuration
class.
This can be done during configuration of your application or similar.
Configuration.setClassLoaderPrecedence(Configuration.ClassLoaderPrecedence.OGM_CLASS_LOADER);
Annotating Entities
@NodeEntity: The basic building block
The @NodeEntity
annotation is used to declare that a POJO class is an entity backed by a node in the graph database.
Entities handled by Neo4j-OGM must have one empty public constructor to allow the library to construct the objects.
Fields on the entity are by default mapped to properties of the node. Fields referencing other node entities (or collections thereof) are linked with relationships.
@NodeEntity
annotations are inherited from super-types and interfaces.
It is not necessary to annotate your domain objects at every inheritance level.
Entity fields can be annotated with annotations like @Property
, @Id
, @GeneratedValue
, @Transient
or @Relationship
.
All annotations live in the org.neo4j.ogm.annotation
package.
Marking a field with the transient modifier has the same effect as annotating it with @Transient
; it won’t be persisted to the graph database.
@NodeEntity
public class Actor extends DomainObject {
@Id @GeneratedValue
private Long id;
@Property(name="name")
private String fullName;
@Property("age") // using value attribute to have a shorter definition
private int age;
@Relationship(type="ACTED_IN", direction=Relationship.Direction.OUTGOING)
private List<Movie> filmography;
}
@NodeEntity(label="Film")
public class Movie {
@Id @GeneratedValue Long id;
@Property(name="title")
private String name;
}
The default label is the simple class name of the annotated entity. There are some rules to determine if parent classes also contribute their label to the child class:
-
the parent class is a non-abstract class (the existing of
@NodeEntity
is optional) -
the parent class is an abstract class and has a
@NodeEntity
annotation -
java.lang.Object
will be ignored -
interfaces do not create an additional label
If the label
(as you can see in the example above) or the value
attribute of the @NodeEntity
annotation is set it will replace the default label applied to the node in the database.
Saving a simple object graph containing one actor and one film using the above annotated objects would result in the following being persisted in Neo4j.
(:Actor:DomainObject {name:'Tom Cruise'})-[:ACTED_IN]->(:Film {title:'Mission Impossible'})
When annotating your objects, you can choose to NOT apply the annotations on the fields. OGM will then use conventions to determine property names in the database for each field.
public class Actor extends DomainObject {
private Long id;
private String fullName;
private List<Movie> filmography;
}
public class Movie {
private Long id;
private String name;
}
In this case, a graph similar to the following would be persisted.
(:Actor:DomainObject {fullName:'Tom Cruise'})-[:FILMOGRAPHY]->(:Movie {name:'Mission Impossible'})
While this will map successfully to the database, it’s important to understand that the names of the properties and relationship types are tightly coupled to the class’s member names. Renaming any of these fields will cause parts of the graph to map incorrectly, hence the recommendation to use annotations.
Please read Non-annotated properties and best practices for more details and best practices on this.
@Properties: dynamically mapping properties to graph
A @Properties
annotation tells Neo4j-OGM to map values of a Map field in a node or relationship entity to properties of
a node or a relationship in the graph.
The property names are derived from field name or prefix
, delimiter
and keys in the Map.
For example Map field with name address
containing following entries:
"street" => "Downing Street"
"number" => 10
will map to following node/relationship properties
address.street=Downing Street
address.number=10
Supported types for keys in the Map
are String
and Enum
.
The values in the Map
can be of any Java type equivalent to Cypher types.
If full type information is provided other Java types are also supported.
If annotation parameter allowCast
is set to true then types that can be cast to corresponding Cypher types are allowed as well.
The original type cannot be deduced and the value will be deserialized to corresponding type - e.g.
when Integer instance is put to Map<String, Object> it will be deserialized as Long .
|
@NodeEntity
public class Student {
@Properties
private Map<String, Integer> properties = new HashMap<>();
@Properties
private Map<String, Object> properties = new HashMap<>();
}
Runtime managed labels
As stated above, the label applied to a node is the contents of the @NodeEntity
label property, or if not specified, it will default to the simple class name of the entity.
Sometimes it might be necessary to add and remove additional labels to a node at runtime.
We can do this using the @Labels
annotation.
Let’s provide a facility for adding additional labels to the Student
entity:
@NodeEntity
public class Student {
@Labels
private List<String> labels = new ArrayList<>();
}
Now, upon save, the node’s labels will correspond to the entity’s class hierarchy plus whatever the contents of the backing field are.
We can use one @Labels
field per class hierarchy - it should be exposed or hidden from sub-classes as appropriate.
Runtime labels must not conflict with static labels defined on node entities.
In a typical situation Neo4j-OGM issues one request per node entity type when saving node entities to the database. Using many distinct labels will result into many requests to the database (one request per unique combination of labels). |
@Relationship: Connecting node entities
Every field of an entity that references one or more other node entities is backed by relationships in the graph. These relationships are managed by Neo4j-OGM automatically.
The simplest kind of relationship is a single object reference pointing to another entity (1:1).
In this case, the reference does not have to be annotated at all, although the annotation may be used to control the direction and type of the relationship.
When setting the reference, a relationship is created when the entity is persisted.
If the field is set to null
, the relationship is removed.
@NodeEntity
public class Movie {
...
private Actor topActor;
}
It is also possible to have fields that reference a set of entities (1:N). Neo4j-OGM supports the following types of entity collections:
-
java.util.Vector
-
java.util.List
, backed by ajava.util.ArrayList
-
java.util.SortedSet
, backed by ajava.util.TreeSet
-
java.util.Set
, backed by ajava.util.HashSet
-
Arrays
@NodeEntity
public class Actor {
...
@Relationship(type = "TOP_ACTOR", direction = Relationship.Direction.INCOMING)
private Set<Movie> topActorIn;
@Relationship("ACTS_IN") // same meaning as above but using the value attribute
private Set<Movie> movies;
}
For graph to object mapping, the automatic transitive loading of related entities depends on the depth of the horizon specified on the call to Session.load()
.
The default depth of 1 implies that related node or relationship entities will be loaded and have their properties set, but none of their related entities will be populated.
If this Set
of related entities is modified, the changes are reflected in the graph once the root object (Actor
, in this case) is saved.
Relationships are added, removed or updated according to the differences between the root object that was loaded and the corresponding one that was saved..
Neo4j-OGM ensures by default that there is only one relationship of a given type between any two given entities.
The exception to this rule is when a relationship is specified as either OUTGOING
or INCOMING
between two entities of the same type.
In this case, it is possible to have two relationships of the given type between the two entities, one relationship in either direction.
If you don’t care about the direction then you can specify direction=Relationship.Direction.UNDIRECTED
which will guarantee that the path between two node entities is navigable from either side.
For example, consider the PARTNER
relationship between two companies, where (A)-[:PARTNER_OF]→(B)
implies (B)-[:PARTNER_OF]→(A)
.
The direction of the relationship does not matter; only the fact that a PARTNER_OF
relationship exists between these two companies is of importance.
Hence an UNDIRECTED
relationship is the correct choice, ensuring that there is only one relationship of this type between two partners and navigating between them from either entity is possible.
The direction attribute on a |
Using more than one relationship of the same type
In some cases, you want to model two different aspects of a conceptual relationship using the same relationship type. Here is a canonical example:
@NodeEntity
class Person {
private Long id;
@Relationship(type="OWNS")
private Car car;
@Relationship(type="OWNS")
private Pet pet;
...
}
This will work just fine, however, please be aware that this is only because the end node types (Car and Pet) are different types.
If you wanted a person to own two cars, for example, then you’d have to use a Collection
of cars or use differently-named relationship types.
Ambiguity in relationships
In cases where the relationship mappings could be ambiguous, the recommendation is that:
-
The objects be navigable in both directions.
-
The
@Relationship
annotations are explicit.
Examples of ambiguous relationship mappings are multiple relationship types that resolve to the same types of entities, in a given direction, but whose domain objects are not navigable in both directions.
Ordering
Neo4j doesn’t have any ordering on relationships, so the relationships are fetched without any specific ordering. If you want to impose order on collections of relationships you have several options:
-
use a
SortedSet
and implementComparable
-
sort relationships in
@PostLoad
annotated method
You can sort either by a property of a related node or by relationship property. To sort by relationship property you need to use a relationship entity. See @RelationshipEntity: Rich relationships.
@RelationshipEntity: Rich relationships
To access the full data model of graph relationships, POJOs can also be annotated with @RelationshipEntity
, making them relationship entities.
Just as node entities represent nodes in the graph, relationship entities represent relationships.
Such POJOs allow you to access and manage properties on the underlying relationships in the graph.
Fields in relationship entities are similar to node entities, in that they’re persisted as properties on the relationship.
For accessing the two endpoints of the relationship, two special annotations are available: @StartNode
and @EndNode
.
A field annotated with one of these annotations will provide access to the corresponding endpoint, depending on the chosen annotation.
For controlling the relationship-type a String
attribute called type
is available on the @RelationshipEntity
annotation.
Like the simple strategy for labelling node entities, if this is not provided then the name of the class is used to derive the relationship type,
although it’s converted into SNAKE_CASE to honour the naming conventions of Neo4j relationships.
As of the current version of Neo4j-OGM, the type
must be specified on the @RelationshipEntity
annotation as well as its corresponding @Relationship
annotations.
This can also be done without naming the attribute but only providing the value.
You must include |
@NodeEntity
public class Actor {
Long id;
@Relationship(type="PLAYED_IN") private Role playedIn;
}
@RelationshipEntity(type = "PLAYED_IN")
public class Role {
@Id @GeneratedValue private Long relationshipId;
@Property private String title;
@StartNode private Actor actor;
@EndNode private Movie movie;
}
@NodeEntity
public class Movie {
private Long id;
private String title;
}
Note that the Actor
also contains a reference to a Role
.
This is important for persistence, even when saving the Role
directly, because paths in the graph are written starting with nodes first and then relationships are created between them.
Therefore, you need to structure your domain models so that relationship entities are reachable from node entities for this to work correctly.
Additionally, Neo4j-OGM will not persist a relationship entity that doesn’t have any properties defined.
If you don’t want to include properties in your relationship entity then you should use a plain @Relationship
instead.
Multiple relationship entities which have the same property values and relate the same nodes are indistinguishable from each other and are represented as a single relationship by Neo4j-OGM.
The |
A note on JSON serialization
Looking at the example given above the circular dependency on the class level between the node and the rich relationship can easily be spotted.
It will not have any effect on your application as long as you do not serialize the objects.
One kind of serialization that is used today is JSON serialization using the Jackson mapper.
This mapper library is often used in connection with other frameworks like Spring or Java EE and their corresponding web modules.
Traversing the object tree it will hit the part when it visits a Role
after visiting an Actor
.
Obvious it will then find the Actor
object and visit this again, and so on.
This will end up in a StackOverflowError
.
To break this parsing cycle it is mandatory to support the mapper by providing annotation to your class(es).
This can be done by adding either @JsonIgnore
on the property that causes the loop or @JsonIgnoreProperties
.
@NodeEntity
public class Actor {
Long id;
// Needs knowledge about the attribute "title" in the relationship.
// Applying JsonIgnoreProperties like this ignores properties of the attribute itself.
@JsonIgnoreProperties("actor")
@Relationship(type="PLAYED_IN") private Role playedIn;
}
@RelationshipEntity(type="PLAYED_IN")
public class Role {
@Id @GeneratedValue private Long relationshipId;
@Property private String title;
// Direct way to suppress the serialization.
// This ignores the whole actor attribute.
@JsonIgnore
@StartNode private Actor actor;
@EndNode private Movie movie;
}
Entity identifier
Every node and relationship persisted to the graph must have an ID. Neo4j-OGM uses this to identify and re-connect the entity to the graph in memory. Identifier may be either a primary id or a native graph id (the technical id attributed by Neo4j at node creation time).
For primary id use the @Id
on a field of any supported type or a field with provided AttributeConverter
.
A unique index is created for such property (if index creation is enabled).
User code should either set the id manually when the entity instance is created or id generation strategy should be used.
It is not possible to store an entity with null id value and no generation strategy.
Specifying primary id on a relationship entity is possible, but lookups by this id are slow, because Neo4j database doesn’t support schema indexes on relationships. |
For native graph id use @Id @GeneratedValue
(with default strategy InternalIdStrategy
).
The field type must be Long
.
This id is assigned automatically upon saving the entity to the graph and user code should never assign a value to it.
It must not be a primitive type because then an object in a transient state cannot be represented, as the default value 0 would point to the reference node. |
Do not rely on this ID for long running applications. Neo4j will reuse deleted node ID’s. It is recommended users come up with their own unique identifier for their domain objects (or use a UUID). |
An entity can be looked up by this either type of id by using Session.load(Class<T>, ID)
and Session.loadAll(Class<T>, Collection<ID>)
methods.
It is possible to have both natural and native id in one entity. In such situation lookups prefer the primary id.
If the field of type Long
is simply named 'id' then it is not necessary to annotate it with @Id @GeneratedValue
as Neo4j-OGM will use it automatically as native id.
Entity Equality
Entity equality can be a grey area.
There are many debatable issues, such as whether natural keys or database identifiers best describe equality and the effects of versioning over time.
Neo4j-OGM does not impose a dependency upon a particular style of equals()
or hashCode()
implementation.
The native or custom id field are directly checked to see if two entities represent the same node and a 64-bit hash code is used for dirty checking, so you’re not forced to write your code in a certain way!
You should write your equals and hashCode in a domain specific way for managed entities. We strongly advise developers to not use the native id described by a Long field in combination with @Id @GeneratedValue in these methods.
This is because when you first persist an entity, its hashcode changes because Neo4j-OGM populates the database ID on save.
This causes problems if you had inserted the newly created entity into a hash-based collection before saving.
|
Id Generation Strategy
If the @Id
annotation is used on its own it is expected that the field will be set by the application code.
To automatically generate and assign a value of the property the annotation @GeneratedValue
can be used.
The @GeneratedValue
annotation has optional parameter strategy
, which can be used to provide a custom id generation strategy.
The class must implement org.neo4j.ogm.id.IdStrategy
interface.
The strategy class can either supply no argument constructor - in which case Neo4j-OGM will create an instance of the strategy and call it.
For situations where some external context is needed an externally created instance can be registered with SessionFactory by using
SessionFactory.register(IdStrategy)
.
Optimistic locking with @Version annotation
Optimistic locking is supported by Neo4j-OGM to provide concurrency control.
To use optimistic locking define a field annotated with @Version
annotation.
The field is then managed by Neo4j-OGM and used to perform optimistic locking checks when updating entities.
The type of the field must be Long
and an entity may contain only one such field.
Typical scenario where optimistic locking is used then looks like follows:
-
new object is created, version field contains
null
value -
when the object is saved the version field is set to 0 by Neo4j-OGM
-
when a modified object is saved the version provided in the object is checked against a version in the database during the update, if successful then the version is incremented both in the object and in the database
-
if another transaction modified the object in the meantime (and therefore incremented the version) then this is detected and an
OptimisticLockingException
is thrown
Optimistic locking check is performed for
-
updating properties of nodes and relationship entities
-
deleting nodes via
Session.delete(T)
-
deleting relationship entities via
Session.delete(T)
-
deleting relationship entities detected through
Session.save(T)
When an optimistic locking failure happens following operations are performed on the Session:
-
object which failed the optimistic locking check is removed from the context so it can be reloaded
-
in case a default transaction is used it is rolled back
-
in case a manual transaction is used then it is not rolled back, but because the update may contain multiple statements which are checked eagerly it is not defined what updates were actually performed in the database and it is advised to rollback the transaction. If you know you updates consists of single modification you may however choose to reload the object and continue the transaction.
@Property: Optional annotation for property fields
As we touched on earlier, it is not necessary to annotate property fields as they are persisted by default.
Fields that are annotated as @Transient
or with transient
are exempted from persistence.
All fields that contain primitive values are persisted directly to the graph.
All fields convertible to a String
using the conversion services will be stored as a string.
Neo4j-OGM includes default type converters for commonly used types, for a full list see Built-in type conversions.
Custom converters are also specified by using @Convert
- this is discussed in detail later on.
Collections of primitive or convertible values are stored as well. They are converted to arrays of their type or strings respectively.
Node property names can be explicitly assigned by setting the name
attribute.
For example @Property(name="last_name") String lastName
.
The node property name defaults to the field name when not specified.
Property fields to be persisted to the graph must not be declared |
@PostLoad
A method annotated with @PostLoad
will be called once the entity is loaded from the database.
Non-annotated properties and best practices
Neo4j-OGM supports mapping annotated and non-annotated objects models. It’s possible to save any POJO without annotations to the graph, as the framework applies conventions to decide what to do. This is useful in cases when you don’t have control over the classes that you want to persist. The recommended approach, however, is to use annotations wherever possible, since this gives greater control and means that code can be refactored safely without risking breaking changes to the labels and relationships in your graph.
The support for non-annotated domain classes might be dropped in the future, to allow startup optimizations. |
Annotated and non-annotated objects can be used within the same project without issue.
The object graph mapping comes into play whenever an entity is constructed from a node or relationship.
This could be done explicitly during the lookup or create operations of the Session
but also implicitly while executing any graph operation that returns nodes or relationships and expecting mapped entities to be returned.
Entities handled by Neo4j-OGM must have one empty public constructor to allow the library to construct the objects.
Unless annotations are used to specify otherwise, the framework will attempt to map any of an object’s "simple" fields to node properties and any rich composite objects to related nodes. A "simple" field is any primitive, boxed primitive or String or arrays thereof, essentially anything that naturally fits into a Neo4j node property. For related entities the type of a relationship is inferred by the bean property name.
Connecting to the Graph
In order to interact with mapped entities and the Neo4j graph, your application will require a Session
, which is provided by the SessionFactory
.
SessionFactory
The SessionFactory
is needed by Neo4j-OGM to create instances of Session
as required.
This also sets up the object-graph mapping metadata when constructed, which is then used across all Session
objects that it creates.
The packages to scan for domain object metadata should be provided to the SessionFactory
constructor.
The SessionFactory is an expensive object to create because it scans all the requested packages to build up metadata.
It should typically be set up once during life of your application.
|
Create SessionFactory with Configuration
instance
As seen in the configuration section, this is done by providing the SessionFactory
a configuration object:
SessionFactory sessionFactory = new SessionFactory(configuration, "com.mycompany.app.domainclasses");
Create SessionFactory with Driver
instance
This can be done by providing to the SessionFactory
a driver instance:
SessionFactory sessionFactory = new SessionFactory(driver, "com.mycompany.app.domainclasses");
Multiple entity packages
Multiple packages may be provided as well. If you would rather just pass in specific classes you can also do that via an overloaded constructor.
SessionFactory sessionFactory = new SessionFactory(configuration, "first.package.domain", "second.package.domain",...);
Using Neo4j-OGM Session
The Session
provides the core functionality to persist objects to the graph and load them in a variety of ways.
Session Configuration
A Session
is used to drive the object-graph mapping framework.
It keeps track of the changes that have been made to entities and their relationships.
The reason it does this is so that only entities and relationships that have changed get persisted on save, which is particularly efficient when working with large graphs.
Once an entity is tracked by the session, reloading this entity within the scope of the same session will result in the session cache returning the previously loaded entity.
However, the subgraph in the session will expand if the entity or its related entities retrieve additional relationships from the graph.
The lifetime of the Session
can be managed in code.
For example, associated with single fetch-update-save cycle or unit of work.
If your application relies on long-running sessions then you may not see changes made from other users and find yourself working with outdated objects. On the other hand, if your sessions have a too narrow scope then your save operations can be unnecessarily expensive, as updates will be made to all objects if the session isn’t aware of the those that were originally loaded.
There’s therefore a trade off between the two approaches.
In general, the scope of a Session
should correspond to a "unit of work" in your application.
If you want to fetch fresh data from the graph, then this can be achieved by using a new session or clearing the current sessions context using Session.clear()
.
This feature should be used with caution because it will clear the whole cache and it needs to get rebuild on the next operation.
Also Neo4j-OGM won’t be able to do any dirty tracking between the operations that are separated by the Session.clear()
call.
Basic operations
Basic operations are limited to CRUD operations on entities and executing arbitrary Cypher queries; more low-level manipulation of the graph database is not possible.
Given that the Neo4j-OGM framework is driven by Cypher queries alone, there’s no way to work directly with Node
and Relationship
objects in remote server mode.
If you find yourself in trouble because of the omission of these features, then your best option is to write a Cypher query to perform the operations on the nodes/relationships instead.
In general, for low-level, very high-performance operations like complex graph traversals you will get the best performance by writing a server-side extension. For most purposes, though, Cypher will be performant and expressive enough to perform the operations that you need.
Persisting entities
Session
allows to save
, load
, loadAll
and delete
entities with transaction handling and exception translation managed for you.
The eagerness with which objects are retrieved is controlled by specifying the depth argument to any of the load methods.
Entity persistence is performed through the save()
method on the underlying Session
object.
Under the bonnet, the implementation of Session
has access to the MappingContext
that keeps track of the data that has been loaded from Neo4j during the lifetime of the session.
Upon invocation of save()
with an entity, it checks the given object graph for changes compared with the data that was loaded from the database.
The differences are used to construct a Cypher query that persists the deltas to Neo4j before repopulating it’s state based on the response from the database server.
Neo4j-OGM doesn’t automatically commit when a transaction closes, so an explicit call to save(…)
is required in order to persist changes to the database.
@NodeEntity
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
// Store Michael in the database.
Person p = new Person("Michael");
session.save(p);
Save depth
As mentioned previously, save(entity)
is overloaded as save(entity, depth)
, where depth dictates the number of related entities to save starting from the given entity.
The default depth, -1, will persist properties of the specified entity as well as every modified entity in the object graph reachable from it.
This means that all affected objects in the entity model that are reachable from the root object being persisted will be modified in the graph.
This is the recommended approach because it means you can persist all your changes in one request.
Neo4j-OGM is able to detect which objects and relationships require changing, so you won’t flood Neo4j with a bunch of objects that don’t require modification.
You can change the persistence depth to any value, but you should not make it less than the value used to load the corresponding data or you run the risk of not having changes you expect to be made actually being persisted in the graph.
A depth of 0 will persist only the properties of the specified entity to the database.
Please be aware that a depth of 0 for a relationship operation will always also affect the linked nodes.
Specifying the save depth is handy when it comes to dealing with complex collections, that could potentially be very expensive to load.
@NodeEntity
class Movie {
String title;
Actor topActor;
public void setTopActor(Actor actor) {
topActor = actor;
}
}
@NodeEntity
class Actor {
String name;
}
Movie movie = new Movie("Polar Express");
Actor actor = new Actor("Tom Hanks");
movie.setTopActor(actor);
Neither the actor nor the movie has been assigned a node in the graph.
If we were to call session.save(movie)
, then Neo4j-OGM would first create a node for the movie.
It would then note that there is a relationship to an actor, so it would save the actor in a cascading fashion.
Once the actor has been persisted, it will create the relationship from the movie to the actor.
All of this will be done atomically in one transaction.
The important thing to note here is that if session.save(actor)
is called instead, then only the actor will be persisted.
The reason for this is that the actor entity knows nothing about the movie entity - it is the movie entity that has the reference to the actor.
Also note that this behaviour is not dependent on any configured relationship direction on the annotations.
It is a matter of Java references and is not related to the data model in the database.
In the following example, the actor and the movie are both managed entities, having both been previously persisted to the graph:
actor.setBirthyear(1956);
session.save(movie);
In this case, even though the movie has a reference to the actor, the property change on the actor will be persisted by the call to |
In the example below, session.save(user,1)
will persist all modified objects reachable from user
up to one level deep.
This includes posts
and groups
but not entities related to them, namely author
, comments
, members
or location
.
A persistence depth of 0 i.e. session.save(user,0)
will save only the properties on the user, ignoring any related entities.
In this case, fullName
is persisted but not friends, posts or groups.
public class User {
private Long id;
private String fullName;
private List<Post> posts;
private List<Group> groups;
}
public class Post {
private Long id;
private String name;
private String content;
private User author;
private List<Comment> comments;
}
public class Group {
private Long id;
private String name;
private List<User> members;
private Location location;
}
Loading Entities
Entities can be loaded from Neo4j-OGM through the use of the session.loadXXX()
methods or via session.query()
/session.queryForObject()
which will
accept your own Cypher queries (See section below on cypher queries).
Neo4j-OGM includes the concept of persistence horizon (depth).
On any individual request, the persistence horizon indicates how many relationships should be traversed in the graph when loading or saving data.
A horizon of zero means that only the root object’s properties will be loaded or saved, a horizon of 1 will include the root object and all its immediate neighbours, and so on.
This attribute is enabled via a depth
argument available on all session methods, but Neo4j-OGM chooses sensible defaults so that you don’t have to specify the depth attribute unless you want change the default values.
Load depth
By default, loading an instance will map that object’s simple properties and its immediately-related objects (i.e. depth = 1). This helps to avoid accidentally loading the entire graph into memory, but allows a single request to fetch not only the object of immediate interest, but also its closest neighbours, which are likely also to be of interest. This strategy attempts to strike a balance between loading too much of the graph into memory and having to make repeated requests for data.
If parts of your graph structure are deep and not broad (for example a linked-list), you can increase the load horizon for those nodes accordingly. Finally, if your graph will fit into memory, and you’d like to load it all in one go, you can set the depth to -1.
On the other hand when fetching structures which are potentially very "bushy" (e.g. lists of things that themselves have many relationships), you may want to set the load horizon to 0 (depth = 0) to avoid loading thousands of objects most of which you won’t actually inspect.
When loading entities with a custom depth less than the one used previously to load the entity within the session, existing relationships will not be flushed from the session; only new entities and relationships are added.
This means that reloading entities will always result in retaining related objects loaded at the highest depth within the session for those entities.
If it is required to load entities with a lower depth than previously requested, this must be done on a new session, or after clearing your current session with |
Loading DTOs
It is possible to also query arbitrary data from Neo4j and make OGM combine the result in a wrapper object/DTO.
To request a DTO, Neo4j-OGM offers <T> List<T> queryDto(String cypher, Map<String, ?> parameters, Class<T> type)
.
This API might get extended in the next minor/patch versions of Neo4j-OGM.
Query Strategy
WhenNeo4j-OGM loads entities through load*
methods (including ones with filters) it uses LoadStrategy
to generate the RETURN
part of the query.
Available load strategies are
-
schema load strategy - uses metadata on domain entities and pattern comprehensions to retrieve nodes and relationships (default since Neo4j-OGM 3.0)
-
path load strategy - uses paths from root node to fetch related nodes,
p=(n)-[0..]-()
(default before Neo4j-OGM 3.0)
The strategy can be overridden globally by calling SessionFactory.setLoadStrategy(strategy)
or for single session only
(e.g. when different strategy is more effective for given query) by calling Session.setLoadStrategy(strategy)
Cypher queries
Cypher is Neo4j’s powerful query language. It is understood by all the different drivers in Neo4j-OGM which means that your application code should run identically, whichever driver you choose to use.
The Session
also allows execution of arbitrary Cypher queries via its query
and queryForObject
methods.
Cypher queries that return tabular results should be passed into the query
method which returns an Result
.
This consists of QueryStatistics
representing statistics of modifying cypher statements if applicable, and an Iterable<Map<String,Object>>
containing the raw data, which can be either used as-is or converted into a richer type if needed.
The keys in each Map
correspond to the names listed in the return clause of the executed Cypher query.
queryForObject
specifically queries for entities and as such, queries supplied to this method must return nodes and not individual properties.
Query methods that retrieve mapped objects may be used in cases where the query generated by load strategy does not have sufficient performance.
Such queries should return nodes and optionally relationships. For a relationship to be mapped both start and end node must be returned.
Query methods returning particular domain type collect the result from all result columns and nested structures in these
(e.g. collected lists, maps etc..) and return as single Iterable<T>
.
Use Result Session.query(java.lang.String, java.util.Map<java.lang.String,?>)
to retrieve only objects in particular column.
In the current version, custom queries do not support paging, sorting or a custom depth. In addition, it does not support mapping a path to domain entities, as such, a path should not be returned from a Cypher query. Instead, return nodes and relationships to have them mapped to domain entities. Modifications made to the graph via Cypher queries directly will not be reflected in your domain objects within the session. |
Sorting and paging
Neo4j-OGM supports Sorting and Paging of results when using the Session object. The Session object methods take independent arguments for Sorting and Pagination
Iterable<World> worlds = session.loadAll(World.class,
new Pagination(pageNumber,itemsPerPage), depth)
Iterable<World> worlds = session.loadAll(World.class,
new SortOrder().add("name"), depth)
Iterable<World> worlds = session.loadAll(World.class,
new SortOrder().add(SortOrder.Direction.DESC,"name"))
Iterable<World> worlds = session.loadAll(World.class,
new SortOrder().add("name"), new Pagination(pageNumber,itemsPerPage))
Neo4j-OGM does not yet support sorting and paging on custom queries. |
Transactions
Neo4j is a transactional database, only allowing operations to be performed within transaction boundaries.
Transactions can be managed explicitly by calling the beginTransaction()
method on the Session
followed by a commit()
or rollback()
as required.
try (Transaction tx = session.beginTransaction()) {
Person person = session.load(Person.class,personId);
Concert concert= session.load(Concert.class,concertId);
Hotel hotel = session.load(Hotel.class,hotelId);
buyConcertTicket(person,concert);
bookHotel(person, hotel);
tx.commit();
} catch (SoldOutException e) {
tx.rollback();
}
make sure to always close the transaction by wrapping it in a try-with-resources block or by calling close() in a finally block.
|
In the example above, the transaction is committed only when both, a concert ticket and hotel room, are available, otherwise, neither booking is made.
If you do not manage a transaction in this manner, auto commit transactions are provided implicitly for Session
methods such as save
, load
, delete
, execute
and so on.
Transactions are by default READ_WRITE
but can also be opened as READ_ONLY
.
Transaction tx = session.beginTransaction(Transaction.Type.READ_ONLY);
...
This is important for clustering where the type of transaction is used to route requests to servers.
Native property types
Neo4j distinguishes between property, structural and composite types. While you can map attributes of Neo4j-OGM entities very easily to composite types, most of the attributes are usually property types. Please read the example using a custom converter for composite types for more information about the mapping of composite types.
The most important property types are
-
Number
-
String
-
Boolean
-
The spatial type
Point
-
Temporal types:
Date
,Time
,LocalTime
,DateTime
,LocalDateTime
andDuration
Number
has two subtypes (Integer
and Float
).
Those are not the Java types with the same name but Neo4j specific types that map to long
and double
respectively.
Please refer to both the Cypher and Java-Driver manual for further information about the type system.
While you have to take a bit of care when modelling entities with numeric attributes (in regards of precession and scale), mapping of numbers, strings and boolean attributes is pretty much straight forward. Temporal and spatial types however made their first appearances in Neo4j 3.4. Therefore OGM provided type conversion for those to store them as string or numeric types. In particular, it maps temporal types onto ISO 8601 formatted strings and spatial types onto a composite, map based structure.
Starting with Neo4j-OGM 3.2, OGM provides dedicated support for Neo4j’s temporal and spatial types.
Supported drivers
Neo4j-OGM supports all Neo4j temporal and spatial types for the Bolt driver. Since Neo4j-OGM 4.0, this supported is included with the Bolt-Module and doesn’t need additional dependencies.
Opt-in to use native types
Using native types for temporal and spatial property types is a behaviour changing feature, as it will turn the default type conversion off and dates are neither written to nor read from strings anymore. Therefore it is an opt-in feature.
To opt-in, please first add the corresponding module for your driver and
than use the new configuration property use-native-types
:
ogm.properties | Java Configuration |
---|---|
|
|
Once enabled, native types are used for all attributes of all node- and relationship-entities and also for all parameters passed through the OGM Session
interface.
Mapping of native types
The following table describes how Neo4j temporal and spatial property types are mapped to attributes of Neo4j-OGM entities:
Neo4j type | Neo4j-OGM type |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
One variant of a Neo4j-OGM spatial point** |
* The Neo4j Duration
can either be a Java 8 Duration
or Period
with the least common denominator being a TemporalAmount
.
A Java 8 duration always deals with an exact number of seconds while periods take daylight saving times and others into account when added to instants.
If you are sure that you only deal with one or the other, you just use an explicit mapping to either java.time.Duration
or java.time.Period
.
** There is no generic Java type representing a spatial point. As OGM supports different ways of connecting to Neo4j it cannot expose either the Java drivers or the internal representation of a point, so it provides a point of its own. Please read the next section to learn which concrete classes Neo4j-OGM offers for a point.
Mapping of Neo4j spatial types
Neo4j supports four slightly different property types for spatial points, see Spatial values.
All variations of the Point
type are backed by an index and therefore perform very well in queries.
The main difference between them is the coordinate reference system.
A point can either be stored in a geographic coordinate system with longitude and latitude or in a cartesian system with x and y.
If you add the third dimension, you add height or a z-axis.
Geographic coordinate systems are based on a spheroidal surface and define a position on a sphere in terms of angles.
Attributes of type Point
in Neo4j having geographic coordinates return longitude
and latitude
with a fixed reference
system of WGS-84 (SRID 4326, the same one that most GPS devices and many online mapservers use).
3-dimensional geographic coordinates have a reference system of WGS-84-3D with the SRID 4979.
Cartesian coordinate systems deal with locations in an euclidean space and are not projected.
Attributes of type Point
in Neo4j having cartesian coordinates return x
and y
, their SRID is 7203 respectively 9157.
The important take-aways for modelling your domain is the fact that points with different coordinate systems are not comparable without undergoing transformation.
The same attribute of a node should always be using the same coordinate system than all the other nodes with the same label.
Otherwise the distance
function and comparisons dealing with multiple Points
will return literal null
.
To make modelling a domain less error prone, Neo4-OGM provides four distinct types that you can use in your Neo4j entities:
Neo4j-OGM type | Neo4j point type |
---|---|
|
A point with |
|
A point with |
|
A point with |
|
A point with |
* Neo4j uses interally x
, y
(and z
) exclusive and provides aliases for longitude
, latitude
(and height
).
Use-cases for the geographic points are all kinds of stuff you usually find on a map. Cartesian points are usefull for indoor navigation and any 2D and 3D modelling. While geographic points deal with degrees as units, cartesian units are undefined by themselves and can be any unit like metres or feet.
Note that the Neo4j-OGM points don’t share a hierarchy usable outside internals of Neo4j-OGM on purpose. It should help you to make an informed decision which coordinate system to use.
Type Conversion
The object-graph mapping framework provides support for default and bespoke type conversions, which allow you to configure how certain data types are mapped to nodes or relationships in Neo4j. If you start with a new Neo4j project on Neo4j 3.4+, you should consider using the native type support of OGM for all temporal types.
Built-in type conversions
Neo4j-OGM will automatically perform the following type conversions:
-
Any object that extends
java.lang.Number
(includingjava.math.BigInteger
andjava.math.BigDecimal
) to a String property -
binary data (as
byte[]
orByte[]
) to base-64 String as Cypher does not support byte arrays -
java.lang.Enum
types using the enum’sname()
method andEnum.valueOf()
-
java.util.Date
to a String in the ISO 8601 format: "yyyy-MM-dd’T’HH:mm:ss.SSSXXX" (usingDateString.ISO_8601
) -
java.time.Instant
to a String in the ISO 8601 with timezone format: "yyyy-MM-dd’T’HH:mm:ss.SSSZ" (usingDateTimeFormatter.ISO_INSTANT
) -
java.time.LocalDate
to a String in the ISO 8601 with format: "yyyy-MM-dd" (usingDateTimeFormatter.ISO_LOCAL_DATE
) -
java.time.LocalDateTime
to a String in the ISO 8601 with format: "yyyy-MM-dd’T’HH:mm:ss" (usingDateTimeFormatter.ISO_LOCAL_DATE_TIME
) -
java.time.OffsetDateTime
to a String in the ISO 8601 with format: "YYYY-MM-dd’T’HH:mm:ss+01:00" / "YYYY-MM-dd’T’HH:mm:ss’Z'" (usingDateTimeFormatter.ISO_OFFSET_DATE_TIME
)
java.time.Instant
based dates are stored in the database using UTC.
Two dedicated annotations are provided to modify the date conversion:
-
@DateString
-
@DateLong
They need to be applied to an attribute for a custom string format or in case you want to store a date or datetime value as long:
public class MyEntity {
@DateString("yy-MM-dd")
private Date entityDate;
}
Alternatively, if you want to store java.util.Date
or java.time.Instant
as long values, use the @DateLong
annotation:
public class MyEntity {
@DateLong
private Date entityDate;
}
Collections of primitive or convertible values are also automatically mapped by converting them to arrays of their type or strings respectively.
Arrays are not supported for java.time.Instant , java.time.LocalDate , java.time.LocalDateTime , java.time.OffsetDateTime .
Collections are not supported for java.time.Instant .
|
Lenient conversion
It is possible to explicitly assign the build-in converter annotations to the corresponding fields.
This provides the advantage of being able to use the lenient
attribute that will get be read by the converters.
The supported annotations are @DateString
, @EnumString
and @NumberString
.
.Example of lenient converter usage
public class MyEntity {
@DateString(lenient = true)
private Date entityDate;
}
The lenient feature is currently only supported by string-based converters to allow the conversion of blank strings from the database.
Custom Type Conversion
In order to define bespoke type conversions for particular members, you can annotate a field with @Convert
.
One of either two convert implementations can be used.
For simple cases where a single property maps to a single field, with type conversion, specify an implementation of AttributeConverter
.
public class MoneyConverter implements AttributeConverter<DecimalCurrencyAmount, Integer> {
@Override
public Integer toGraphProperty(DecimalCurrencyAmount value) {
return value.getFullUnits() * 100 + value.getSubUnits();
}
@Override
public DecimalCurrencyAmount toEntityAttribute(Integer value) {
return new DecimalCurrencyAmount(value / 100, value % 100);
}
}
You could then apply this to your class as follows:
@NodeEntity
public class Invoice {
@Convert(MoneyConverter.class)
private DecimalCurrencyAmount value;
...
}
When more than one node property is to be mapped to a single field, use: CompositeAttributeConverter
.
/**
* This class maps latitude and longitude properties onto a Location type that encapsulates both of these attributes.
*/
public class LocationConverter implements CompositeAttributeConverter<Location> {
@Override
public Map<String, ?> toGraphProperties(Location location) {
Map<String, Double> properties = new HashMap<>();
if (location != null) {
properties.put("latitude", location.getLatitude());
properties.put("longitude", location.getLongitude());
}
return properties;
}
@Override
public Location toEntityAttribute(Map<String, ?> map) {
Double latitude = (Double) map.get("latitude");
Double longitude = (Double) map.get("longitude");
if (latitude != null && longitude != null) {
return new Location(latitude, longitude);
}
return null;
}
}
And just as with an AttributeConverter
, a CompositeAttributeConverter
could be applied to your class as follows:
@NodeEntity
public class Person {
@Convert(LocationConverter.class)
private Location location;
...
}
Filters
Filters provide a mechanism for customising the where clause of Cypher generated by Neo4j-OGM.
They can be chained together with boolean operators, and associated with a comparison operator.
Additionally, each filter contains a FilterFunction
.
A filter function can be provided when the filter is instantiated, otherwise, by default a PropertyComparison
is used.
In the example below, we are return a collection containing any satellites that are manned.
Collection<Satellite> satellites = session.loadAll(Satellite.class, new Filter("manned", ComparisonOperator.EQUALS, true));
Filter mannedFilter = new Filter("manned", ComparisonOperator.EQUALS, true);
Filter landedFilter = new Filter("landed", ComparisonOperator.EQUALS, false);
Filters satelliteFilter = mannedFilter.and(landedFilter);
The filters should be considered as immutable. In previous versions, you could change filter values after instantiation, this is not the case anymore. |
Events
Neo4j-OGM supports persistence events. This section describes how to intercept update and delete events.
You may also check the @PostLoad
annotation which is described here.
Event types
There are four types of events:
Event.LIFECYCLE.PRE_SAVE Event.LIFECYCLE.POST_SAVE Event.LIFECYCLE.PRE_DELETE Event.LIFECYCLE.POST_DELETE
Events are fired for every @NodeEntity
or @RelationshipEntity
object that is created, updated or deleted, or otherwise affected by a save or delete request.
This includes:
-
The top-level objects or objects being created, modified or deleted.
-
Any connected objects that have been modified, created or deleted.
-
Any objects affected by the creation, modification or removal of a relationship in the graph.
Events will only fire when one of the |
Interfaces
The events mechanism introduces two new interfaces, Event
and EventListener
.
The Event interface
The Event
interface is implemented by PersistenceEvent
.
Whenever an application wishes to handle an event it will be given an instance of Event
, which exposes the following methods:
public interface Event {
Object getObject();
LIFECYCLE getLifeCycle();
enum LIFECYCLE {
PRE_SAVE, POST_SAVE, PRE_DELETE, POST_DELETE
}
}
The event listener interface
The EventListener
interface provides methods allowing implementing classes to handle each of the different Event
types:
public interface EventListener {
void onPreSave(Event event);
void onPostSave(Event event);
void onPreDelete(Event event);
void onPostDelete(Event event);
}
Although the |
Registering an EventListener
There are two ways to register an event listener:
-
on an individual
Session
-
across multiple sessions by using a
SessionFactory
In this example we register an anonymous EventListener to inject a UUID onto new objects before they’re saved
class AddUuidPreSaveEventListener implements EventListener {
void onPreSave(Event event) {
DomainEntity entity = (DomainEntity) event.getObject():
if (entity.getId() == null) {
entity.setUUID(UUID.randomUUID());
}
}
void onPostSave(Event event) {
}
void onPreDelete(Event event) {
}
void onPostDelete(Event event) {
}
EventListener eventListener = new AddUuidPreSaveEventListener();
// register it on an individual session
session.register(eventListener);
// remove it.
session.dispose(eventListener);
// register it across multiple sessions
sessionFactory.register(eventListener);
// remove it.
sessionFactory.deregister(eventListener);
It is possible and sometimes desirable to add several EventListener objects to the session, depending on the application’s requirements. For example, our business logic might require us to add a UUID to a new object, as well as manage wider concerns such as ensuring that a particular persistence event won’t leave our domain model in a logically inconsistent state. It is usually a good idea to separate these concerns into different objects with specific responsibilities, rather than having one single object try to do everything. |
Using the EventListenerAdapter
The EventListener
above is fine, but we’ve had to create three methods for events we don’t intend to handle.
It would be preferable if we didn’t have to do this each time we needed an EventListener
.
The EventListenerAdapter
is an abstract class providing a no-op implementation of the EventListener
interface.
If you don’t need to handle all the different types of persistence event you can create a subclass of EventListenerAdapter
instead and override just the methods for the event types you’re interested in.
For example:
class PreSaveEventListener extends EventListenerAdapter {
@Override
void onPreSave(Event event) {
DomainEntity entity = (DomainEntity) event.getObject();
if (entity.id == null) {
entity.UUID = UUID.randomUUID();
}
}
}
Disposing of an EventListener
Something to bear in mind is that once an EventListener
has been registered it will continue to respond to all persistence events.
Sometimes you may want only to handle events for a short period of time, rather than for the duration of the entire session.
If you are done with an EventListener
you can stop it from firing any more events by invoking session.dispose(…)
, passing in the EventListener
to be disposed of.
The process of collecting persistence events prior to dispatching them to any |
To remove an event listener across multiple sessions use the deregister
method on the SessionFactory
.
Connected objects
As mentioned previously, events are not only fired for the top-level objects being saved but for all their connected objects as well.
Connected objects are any objects reachable in the domain model from the top-level object being saved. Connected objects can be many levels deep in the domain model graph.
In this way, the event mechanism allows us to capture events for objects that we didn’t explicitly save ourselves.
// initialise the graph
Folder folder = new Folder("folder");
Document a = new Document("a");
Document b = new Document("b");
folder.addDocuments(a, b);
session.save(folder);
// change the names of both documents and save one of them
a.setName("A");
b.setName("B");
// because `b` is reachable from `a` (via the common shared folder) they will both be persisted,
// with PRE_SAVE and POST_SAVE events being fired for each of them
session.save(a);
Events and types
When we delete a type, all the nodes with a label corresponding to that type are deleted in the graph.
The affected objects are not enumerated by the events mechanism (they may not even be known).
Instead, _DELETE
events will be raised for the type:
// 2 events will be fired when the type is deleted.
// - PRE_DELETE Document.class
// - POST_DELETE Document.class
session.delete(Document.class);
Events and collections
When saving or deleting a collection of objects, separate events are fired for each object in the collection, rather than for the collection itself.
Document a = new Document("a");
Document b = new Document("b");
// 4 events will be fired when the collection is saved.
// - PRE_SAVE a
// - PRE_SAVE b
// - POST_SAVE a
// - POST_SAVE b
session.save(Arrays.asList(a, b));
Event ordering
Events are partially ordered.
PRE_
events are guaranteed to fire before any POST_
event within the same save
or delete
request.
However, the internal ordering of the PRE_
events and POST_
events with the request is undefined.
Document a = new Document("a");
Document b = new Document("b");
// Although the save order of objects is implied by the request, the PRE_SAVE event for `b`
// may be fired before the PRE_SAVE event for `a`, and similarly for the POST_SAVE events.
// However, all PRE_SAVE events will be fired before any POST_SAVE event.
session.save(Arrays.asList(a, b));
Relationship events
The previous examples show how events fire when the underlying node representing an entity is updated or deleted in the graph. Events are also fired when a save or delete request results in the modification, addition or deletion of a relationship in the graph.
For example, if you delete a Document
object that is contained in the documents collection of a Folder
,
events will be fired for the Document
as well as the Folder
,
to reflect the fact that the relationship between the folder and the document has been removed in the graph.
Document
attached to a Folder
Folder folder = new Folder();
Document a = new Document("a");
folder.addDocuments(a);
session.save(folder);
// When we delete the document, the following events will be fired
// - PRE_DELETE a
// - POST_DELETE a
// - PRE_SAVE folder (1)
// - POST_SAVE folder
session.delete(a);
1 | Note that the folder events are _SAVE events, not _DELETE events.
The folder was not deleted. |
The event mechanism does not try to synchronise your domain model.
In this example, the folder is still holding a reference to the |
Event uniqueness
The event mechanism guarantees to not fire more than one event of the same type for an object in a save or delete request.
// Even though we're making changes to both the folder node, and its relationships,
// only one PRE_SAVE and one POST_SAVE event will be fired.
folder.removeDocument(a);
folder.setName("newFolder");
session.save(folder);
Testing
There are several options when it comes to testing. You can either choose to use an embedded instance via the Test Harness or make use of external libraries like Testcontainers Neo4j.
Test harness
Doing integration testing with Neo4j-OGM requires a few basic steps :
-
Add the
org.neo4j.test:neo4j-harness
artifact to your Maven / Gradle configuration -
Declare the
Neo4jRule
JUnit rule, to setup a Neo4j test server (JUnit4 and this rule is not necessary to run the Test Harness) -
Setup Neo4j-OGM configuration and
SessionFactory
An example of a full running configuration can be found in the issue templates.
Log levels
When running unit tests, it can be useful to see what Neo4j-OGM is doing, and in particular to see the Cypher requests being transferred between your application and the database.
Neo4j-OGM uses slf4j
along with Logback as its logging framework and by default the log level for all Neo4j-OGM components is set to WARN, which does not include any Cypher output.
To change Neo4j-OGM log level, create a file logback-test.xml in your test resources folder, configured as shown below:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
</encoder>
</appender>
<!--
~ Set the required log level for Neo4j-OGM components here.
~ To just see Cypher statements set the level to "info"
~ For finer-grained diagnostics, set the level to "debug".
-->
<logger name="org.neo4j.ogm" level="info" />
<root level="warn">
<appender-ref ref="console" />
</root>
</configuration>