There are several layers of abstraction involved getting values of a given type into Neo4j and out of the database again. Chosing one technology to write a value and another to retrieve it, can lead to hard to spot errors. This tip demonstrate some combinations of drivers that work well to together and explains why certain values end up in the database with a different type than on the Java-side of things.
1. Problem
You have a node with a numeric value in your database.
On the client side, you’ll expect an int or java.lang.Integer but are getting back a long or java.lang.Long. from you connection
2. Solution
There is no "one-size-fits" all solution to this problem, but merely an explanation and a recommendation.
The database itself supports a lot of different types like byte, short, int, long, String, arrays thereof and many more.
This types are however database internal.
You interact with these internal types as soon as you interact with an embedded database without using a driver.
It doesn’t matter if you make direct API calls or interact through Cypher with the database.
| The term driver is used in multiple places: First and foremost it’s a library that implements a certain protocol to connect a client to a database. The Neo4j-Java-Driver is such a driver. It implements the Bolt protocol. Neo4j-OGM also uses the term driver but here, the OGM-Bolt-driver sits on top of the Java-Driver. The OGM-embedded-driver uses the embedded API directly. |
Look at Figure 1 to see how the the Java driver, embedded database, OGM and finally work together:
Cypher itself exposes the following types:
| Cypher Type | Parameter | Result |
|---|---|---|
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
|
|
✔ |
|
|
✔ |
The Java driver maps those types to the following Java types:
| Neo4j type | Java type |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
So in any case you get a long out of your database while you are sure you have stored an int or an Integer in the past, you probably have used an embedded instance with a direct connection.
Having a value with a smaller width in the database than your application should usually not be a problem, apart from a necessary cast.
Having long or double stored and expecting int or float back, you’re risking a loss of precision.
The Java driver will throw a LossyCoercion exception.
| The preferred way of interacting with Neo4j is through a driver. If you access the embedded database directly, you should make sure to access it in a consistent way. In a scenario where this isn’t possible, for example when you are switching from embedded to cluster, you may have to adapter your client code to use the correct type. |
3. Examples
All example code is in the understand-the-type-system project.
In TypeConversionTest we set up an embedded instance having the Bolt-Connector enabled as well. See Listing 1.
this.neo4jDb = Files.createTempDirectory("neo4j.db").toFile();
final BoltConnector bolt = new BoltConnector();
this.graphDatabaseService = new GraphDatabaseFactory().newEmbeddedDatabaseBuilder(neo4jDb)
.setConfig(bolt.type, Connector.ConnectorType.BOLT.name()).setConfig(bolt.enabled, "true")
.setConfig(bolt.listen_address, LOCAL_BOLT_URL).newGraphDatabase();
this.driver = GraphDatabase.driver("bolt://" + LOCAL_BOLT_URL, AuthTokens.none());
The examples consists of write and read commands.
The read commands take an Integer value, store it as a property of a node labeled TestNode and retrieve the nodes id.
The write commands take the nodes id and return the value of the property.
We are dealing throughout the examples with this property:
static final Integer VALUE_TEST_PROPERTY = 42;
3.1. Write and read directly through the API
Writing looks probably like this:
org.neo4j.graphdb.GraphDatabaseServicetry (var transaction = graphDatabaseService.beginTx()) {
final Node node = graphDatabaseService.createNode(Label.label(NODE_LABEL));
node.setProperty(NAME_SOURCE_PROPERTY, "written directly via API");
node.setProperty(NAME_TEST_PROPERTY, value);
transaction.success();
return node.getId();
}
A test confirms that reading through the same API returns 42 as Integer again.
@Test
@DisplayName("Direct API calls should write and read java.lang.Integer as Integer")
public void writingAndReadingViaDirectApi() {
assertEquals(VALUE_TEST_PROPERTY, new WriteViaAPICmd(graphDatabaseService)
.andThen(new ReadViaAPICmd(graphDatabaseService)).apply(VALUE_TEST_PROPERTY),
"Returned not the same value as written");
}
Conclusion: The Integer value written can be read back as as Integer again.
3.2. Write and read with Cypher
Given the Cypher statement in Listing 5, we can this use this for writing via the API and the Java-Driver:
static final String CYPHER_WRITE = "CREATE (n:TestNode) SET n.numericProperty = $numericProperty, n.source = $source RETURN n";
3.2.1. via the API
Writing with Cypher via embedded API can look like this:
org.neo4j.graphdb.GraphDatabaseServicefinal Map<String, Object> parameters = Map.of(NAME_TEST_PROPERTY, value, NAME_SOURCE_PROPERTY,
"written with Cypher via direct connection to embedded");
try (var transaction = graphDatabaseService.beginTx()) {
final Long nodeID = graphDatabaseService.execute(CYPHER_WRITE, parameters)
.map(row -> ((Node) row.get("n")).getId()).stream().findFirst().get();
transaction.success();
return nodeID;
}
Again, the test confirms that reading through the same API returns 42 as Integer:
@Test
@DisplayName("Cypher over API should write and read java.lang.Integer as Integer")
public void writingAndReadingWithCypherOverEmbeddedConnection() {
assertEquals(VALUE_TEST_PROPERTY,
new WriteWithCypherViaEmbeddedCmd(graphDatabaseService)
.andThen(new ReadWithCypherViaEmbeddedCmd(graphDatabaseService)).apply(VALUE_TEST_PROPERTY),
"Returned not the same value as written");
}
3.2.2. via the Java driver
This looks different when using Cypher over the Java driver. That is the point where the Type mapping kicks in.
final Map<String, Object> parameters = Map.of(NAME_TEST_PROPERTY, value, NAME_SOURCE_PROPERTY,
"written with Cypher via Java (Bolt) driver");
try (var session = driver.session()) {
return session.writeTransaction(tx -> tx.run(CYPHER_WRITE, parameters).single().get("n").asNode().id());
}
Now the test looks differen:
We have to case the original property to a Long to make it pass:
@Test
@DisplayName("Cypher over BOLT should write and read java.lang.Integer as Long")
public void writingAndReadingWithCypherOverBoltConnection() {
assertEquals((Long) VALUE_TEST_PROPERTY.longValue(),
new WriteWithCypherViaBoltCmd(driver).andThen(new ReadWithCypherViaBoltCmd(driver)).apply(VALUE_TEST_PROPERTY),
"Returned not the same value as written");
}
The driver has converted the Integer value to Long during write and read.
3.2.3. Mixing access patterns
The test in Listing 10 demonstrates mixed access patterns.
@Test
@DisplayName("Mixing different access methods has to be handled with care")
public void mixingDifferentAccessMethods() {
assertEquals(
(Long) VALUE_TEST_PROPERTY.longValue(), new WriteViaAPICmd(graphDatabaseService)
.andThen(new ReadWithCypherViaBoltCmd(driver)).apply(VALUE_TEST_PROPERTY),
"Bolt should have used the Java-Driver type mapping");
assertEquals(
(Long) VALUE_TEST_PROPERTY.longValue(), new WriteWithCypherViaEmbeddedCmd(graphDatabaseService)
.andThen(new ReadWithCypherViaBoltCmd(driver)).apply(VALUE_TEST_PROPERTY),
"Bolt should have used the Java-Driver type mapping");
assertThrows(ClassCastException.class, () -> new WriteWithCypherViaBoltCmd(driver)
.andThen(new ReadViaAPICmd(graphDatabaseService)).apply(VALUE_TEST_PROPERTY));
assertThrows(ClassCastException.class, () -> new WriteWithCypherViaBoltCmd(driver)
.andThen(new ReadWithCypherViaEmbeddedCmd(graphDatabaseService)).apply(VALUE_TEST_PROPERTY));
}
You’ll be fine if you use Cypher over the Java driver and expect a Long.
However, you’ll cannot expect an Integer written through over a driver to return as Integer.
3.3. OGM
OGM will happily map a smaller property stored to a wider attribute as Listing 11 shows, using either a direct connection or a driver connection. Mapping a wider property to to a smaller attribute will fail.
@Nested
@DisplayName("OGM applies it's own type mapping...")
class OGM {
@Test
@DisplayName("...over embedded")
public void overEmbedded() {
var sessionFactory = new SessionFactory(new EmbeddedDriver(graphDatabaseService),
this.getClass().getPackage().getName());
final Session session = sessionFactory.openSession();
assertAll(session.loadAll(TestNode.class).stream()
.map(node -> () -> assertEquals(VALUE_TEST_PROPERTY.longValue(), node.getNumericProperty())));
}
@Test
@DisplayName("...over Bolt")
public void overBolt() {
var sessionFactory = new SessionFactory(new BoltDriver(driver), this.getClass().getPackage().getName());
final Session session = sessionFactory.openSession();
assertAll(session.loadAll(TestNode.class).stream()
.map(node -> () -> assertEquals(VALUE_TEST_PROPERTY.longValue(), node.getNumericProperty())));
}
}