Neo4j-OGM 3.2 released

Michael Simons
Neo4j Developer Blog
7 min readOct 7, 2019

Current state and future plans of Neo4j-OGM

There is happening so much right now in the world of graphs.
We have just released our first public milestone release of Neo4j 4.0, the upcoming major version of Neo4j, next week the NODES2019 online conference is happening and the team behind Spring Data Neo4j and Neo4j-OGM wasn’t sleeping as well:

SDN/RX was released as a public beta with a fantastic introduction video by Gerrit Meier.
I personally was super happy to find a blog post written by someone else about SDN/RX title “Reactive programming with Neo4j”.
Last but not least, even Jürgen Höller mentioned our work on reactive transactions during the announcement of Spring Framework 5.2.

Photo by delfi de la Rua on Unsplash

But aside from all those amazing new development, we are determined to maintain the SDN+OGM legacy.
Last week, we released Neo4j-OGM 3.2, which is the baseline of Spring Data Neo4j 5.2, released as part of the Spring Data Moore release train.
Neo4j-OGM 3.2 contains all bug fixes and improvements contained in 3.1 since the original release of 3.1, but also the following:

  • Allow configuration of packages to scan in ogm.properties through base-packages.
  • Introduced exception translator to unify exceptions of different transports into an OGM hierarchy.
  • Deprecated OgmPluginInitializer
  • SessionFactory.getDriver() has been replaced with SessionFactory.unwrap(Class<T> clazz) which provides a consistent way to get the underlying Neo4j-OGM driver or the native driver.
  • Add support for containing filter in combination with ignore case.
  • Improved support for enums in Map-properties
  • Improved Kotlin support
  • Supports all native Neo4j 3.5 types in embedded an Bolt modes

We also removed some things:

  • Removed “neo4j.ha.properties.file” property from OGM configuration. Use “neo4j.conf.location” instead.
  • Removed org.neo4j.ogm.autoindex.AutoIndexManager#build. Use org.neo4j.ogm.autoindex.AutoIndexManager#run instead.
  • Removed deprecated and unsupported method org.neo4j.ogm.session.Neo4jSession#setDriver.
  • Removed deprecated @GraphId. Please use a Long field annotated with @Id @GeneratedValue instead.
  • Removed deprecated org.neo4j.ogm.session.Session.doInTransaction(GraphCallback<T>).
  • Removed deprecated and unused ServiceNotFoundException.
  • Removed deprecated org.neo4j.ogm.session.Neo4jException.
  • Removed deprecated org.neo4j.ogm.exception.core.NotFoundException.
  • Removed deprecated org.neo4j.ogm.exception.core.ResultErrorsException

From 3.2.0 onwards, we don’t distribute neo4j-ogm-test anymore. That package contains some test utilities that we are using internally and which we don’t maintain in a stable way to be used from the outside.

Thanks a lot to everyone reporting issues (https://github.com/neo4j/neo4j-ogm/issues), providing pull request or just gave us feedback, on external as well as internal channels.

Neo4j-OGM 3.2 is build and tested against Neo4j 3.4 and 3.5. You can use it already to connect against Neo4j 4.0 over Bolt (using Neo4j in server mode), but this is untested as of now. We aim to ensure compatibility with Neo4j 4.0 with the GA release of Neo4j 4.0

I like to highlight two features of this release, in addition to the overall improvements and stability work:

Improved Kotlin support

Neo4j-OGM is a database to object and vice versa mapper. Especially coming from the database side of things, many aspects are to be considered while building the object graph:

  • Which properties have been loaded?
  • Which relationships have been traversed and how are they returned to the client? Are the returned as a cartesian product along with the main-node, as collections or as projections?
  • Are required properties missing and do we need to execute additional queries?

Those three aspects are somewhat against the concepts of immutable data classes, which are the preferred way in the Kotlin universe to pass around blocks of data.

While the Spring Data infrastructure provides superb support for constructor based creation of entities after all properties and associations have been loaded, we cannot use those infrastructure from Neo4j-OGM as we still want that library to be usable without Spring.

We could however make life easier for all Kotlin fans: final attributes are no longer excluded from mapping. Neo4j-OGM uses field access internally to populate them, but that doesn’t leak into your domain.

How to use Kotlin Data classes then?

Neo4j-OGM 3.2 still needs the no-args constructor on your @NodeEntity- and @RelationshipEntity annotated classes. Luckily, there’s a Maven build plugin for that, the org.jetbrains.kotlin:kotlin-maven-plugin.

Add it to your build like this (kotlin.version being 1.3.50 in our case):

<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<pluginOptions>
<option>no-arg:annotation=org.neo4j.ogm.annotation.NodeEntity</option>
</pluginOptions>
<compilerPlugins>
<plugin>no-arg</plugin>
</compilerPlugins>
<jvmTarget>1.8</jvmTarget>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<source>src/test/java</source>
<source>src/test/kotlin</source>
</sourceDirs>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

This configures the plugin to generate a no-args constructor for all classes annotated with @NodeEntity. Similar can be done for @RelationshipEntity. The constructor will not be visible or usable in your application, so you don’t need to worry about invalid data inside your application. Gradle is of course covered as well, but please refer to the Kotlin documentation.

Having that in a build, you could use classes like the following in your domain:

import org.neo4j.ogm.annotation.*


@NodeEntity
data class MyNode (
@Id @GeneratedValue var dbId: Long? = null,
@Index(unique = true) val name: String,
val description: String,
@Relationship("IS_LINKED_TO", direction = Relationship.OUTGOING)
val otherNodes: List<OtherNode> = emptyList()
)

@NodeEntity
data class OtherNode (
@Id @GeneratedValue var dbId: Long? = null,
@Index(unique = true) val name: String
)

The usage would look like this, where sessionFactory points to an OGM SessionFactory, containing the class definitions above

@Test
fun basicMappingShouldWork() {

var myNode = MyNode(
name = "Node1", description = "A node",
otherNodes = listOf(
OtherNode(name = "o1"), OtherNode(name = "o2")
)
)

val session = sessionFactory.openSession()
session.save(myNode)

myNode = session.load(myNode.dbId!!)
assertThat(myNode.name).isEqualTo("Node1")
assertThat(myNode.description).isEqualTo("A node")
assertThat(myNode.otherNodes)
.hasSize(2)
.extracting("name").containsExactlyInAnyOrder("o1", "o2")
}

This little example shows something else we added to Neo4j-OGM: We added support for reified types on all the methods of Neo4j-OGM Session and SessionFactory that deal with class type parameters. That means, you can either assign the result to a known type or use type parameters (but don’t need to fallback to extracting the Java class). Here are some examples:

val session = sessionFactory.openSession()
val farin : MyNode = session.queryForObject(
"MATCH (n:MyNode {name: \$name}) RETURN n",
mapOf(Pair("name", "Farin")))
assertThat(farin.name).isEqualTo("Farin")val nodes = session.loadAll<MyNode>(Filter("name", ComparisonOperator.EQUALS, "John"))
val returnedNames : Iterable<String> = session.query("MATCH (n:MyNode) RETURN n.name ORDER BY n.name DESC")
session.deleteAll<MyNode>()

Working on this actually originates directly from our R&D work with SDN/RX: Things we find usable and desirable there, we try to port over to Neo4j-OGM.

Native types support

Since Neo4j 3.4, the database has seen many additional interesting and very useful types. Among them are Cypher dates and times, which all map neatly to Java’s JDK 8 java.time types, but also spatial types. Spatial types describes coordinates, either two- or three dimensional in either cartesian or geographic coordinate systems.

Neo4j-OGM has a complicated background and has seen several installments, leading to support

  • HTTP
  • Bolt
  • and embedded connections to a Neo4j database

You’ll find structures and conversions from the HTTP transport all over the place and it has given us a hard time to cater for native types. In the end we decided to support native types (all Cypher dates and times as well as spatial types) only for connections over Bolt and embedded usage.

Photo by Eric Rothermel on Unsplash

To enable this, you’ll need to one of org.neo4j:neo4j-ogm-bolt-native-types or org.neo4j:neo4j-ogm-embedded-native-types to your project. If you’re on Spring Boot, you don’t need to add a version number for the dependencies from Spring Boot 2.2.0.RC1 onwards. Those are part of the managed dependencies.

If you are on Bolt, running Neo4j in server mode, than you have to add

<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-ogm-bolt-driver</artifactId>
<version>${neo4j-ogm.version}</version>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-ogm-bolt-native-types</artifactId>
<version>${neo4j-ogm.version}</version>
</dependency>

to enjoy native types, on embedded you need

<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-ogm-embedded-driver</artifactId>
<version>${neo4j-ogm.version}</version>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-ogm-embedded-native-types</artifactId>
<version>${neo4j-ogm.version}</version>
</dependency>

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, either through ogm.properties:

URI=bolt://neo4j:password@localhost
use-native-types=true

or programmatically configuration:

Configuration configuration = new Configuration.Builder()
.uri("bolt://neo4j:password@localhost")
.useNativeTypes()
.build()

In a Spring Boot 2.2.0 application, you will be able to configure this via a Spring Boot property:

spring.data.neo4j.use-native-types=true

You still need to declare the native types for the transport you want to use as stated above, though. Spring Boot 2.2 will be released this year and is now in release candidate phase.

There’s nothing more to do after that to use one of the following types in your domain:

  • java.time.LocalDate
  • java.time.OffsetTime
  • java.time.LocalTime
  • java.time.ZonedDateTime
  • java.time.LocalDateTime
  • java.time.Duration
  • java.time.Period
  • org.neo4j.ogm.types.spatial.CartesianPoint2d
  • org.neo4j.ogm.types.spatial.CartesianPoint3d
  • org.neo4j.ogm.types.spatial.GeographicPoint2d
  • org.neo4j.ogm.types.spatial.GeographicPoint3d

The time types are mapped the same way as the Java driver for bolt does it (see our manual here https://neo4j.com/docs/driver-manual/1.7/cypher-values/#driver-neo4j-type-system), the additional spatial types are mapped to a

  • To a cartesian point exhibiting x, y (and z)
  • Or to a geographic point with the WGS 84 coordinate reference system exhibiting longitude, latitude (and height). The SRID used for 2D will be 4326, for 3D 4979.

One possible domain class in a Spring Boot / Spring Data application making use of this would be this:

import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;

import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.Index;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;
import org.neo4j.ogm.types.spatial.GeographicPoint2d;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

@NodeEntity("MusicVenue")
@Getter
public class MusicVenueEntity {
@Id
@GeneratedValue
private Long id;

@Index
private String name;

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;

private GeographicPoint2d location;

@Relationship("IS_LOCATED_IN")
@Setter
private CountryEntity foundedIn;

public MusicVenueEntity(
String name, GeographicPoint2d location
) {
this.name = name;
this.location = location;
}
}

What’s next?

We will be working on making the embedded version 4.0 compatible. After that, we have ideas in the pipeline to make better use of https://github.com/classgraph/classgraph to improve the class analysis.

--

--

Michael Simons
Neo4j Developer Blog

👨‍👩‍👦‍👦👨🏻‍💻🚴🏻 — Father, Husband, Programmer, Cyclist. Author of @springbootbuch, founder of @euregjug. Java champion working on @springdata at @neo4j.