From 4b84fce243fed026d05e790886e58489be750e03 Mon Sep 17 00:00:00 2001 From: Stefano Ottolenghi Date: Mon, 21 Jul 2025 21:47:03 +0200 Subject: [PATCH 1/5] draft --- .../modules/ROOT/pages/query-simple.adoc | 11 +- .../modules/ROOT/pages/value-mapping.adoc | 179 ++++++++++++++++++ 2 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 java-manual/modules/ROOT/pages/value-mapping.adoc diff --git a/java-manual/modules/ROOT/pages/query-simple.adoc b/java-manual/modules/ROOT/pages/query-simple.adoc index c1280034..d401441d 100644 --- a/java-manual/modules/ROOT/pages/query-simple.adoc +++ b/java-manual/modules/ROOT/pages/query-simple.adoc @@ -7,6 +7,7 @@ Once you have xref:connect.adoc[connected to the database], you can execute < The xref:result-summary.adoc[summary of execution] returned by the server +[#read] == Read from the database To retrieve information from the database, use the Cypher clause link:{neo4j-docs-base-uri}/cypher-manual/current/clauses/match/[`MATCH`]: @@ -71,16 +73,15 @@ System.out.printf("The query %s returned %d records in %d ms.%n", <1> `records` contains the result as a list of link:https://neo4j.com/docs/api/java-driver/{java-driver-version}/org.neo4j.driver/org/neo4j/driver/Record.html[`Record`] objects <2> `summary` contains the xref:result-summary.adoc[summary of execution] returned by the server -[TIP] -==== Properties inside a link:https://neo4j.com/docs/api/java-driver/{java-driver-version}/org.neo4j.driver/org/neo4j/driver/Record.html[`Record`] object are embedded within link:https://neo4j.com/docs/api/java-driver/{java-driver-version}/org.neo4j.driver/org/neo4j/driver/Value.html[`Value`] objects. To extract and cast them to the corresponding Java types, use `.as()` (eg. `.asString()`, `asInt()`, etc). For example, if the `name` property coming from the database is a string, `record.get("name").asString()` will yield the property value as a `String` object. - For more information, see xref:data-types.adoc[]. -==== + +Another way of extracting values from returned records is by xref:value-mapping.adoc[mapping them objects]. +[#update] == Update the database To update a node's information in the database, use the Cypher clauses link:{neo4j-docs-base-uri}/cypher-manual/current/clauses/match/[`MATCH`] and link:{neo4j-docs-base-uri}/cypher-manual/current/clauses/set/[`SET`]: @@ -129,7 +130,9 @@ System.out.println(summary.counters().containsUpdates()); <3> Create a new `:KNOWS` relationship outgoing from the node bound to `alice` and attach to it the `Person` node named `Bob` +[#delete] == Delete from the database + To remove a node and any relationship attached to it, use the Cypher clause link:{neo4j-docs-base-uri}/cypher-manual/current/clauses/delete/[`DETACH DELETE`]: .Remove the `Alice` node and all its relationships diff --git a/java-manual/modules/ROOT/pages/value-mapping.adoc b/java-manual/modules/ROOT/pages/value-mapping.adoc new file mode 100644 index 00000000..24b1883b --- /dev/null +++ b/java-manual/modules/ROOT/pages/value-mapping.adoc @@ -0,0 +1,179 @@ += Map query results to objects + +When xref:query-simple.adoc#read[working with values coming from a query result], you have to manually extract their properties and cast them to the relevant Java types before you can use them. +For example, to retrieve the `name` property as a string, you have to do `person.get("name").asString()`. + +With the driver's Value Mapping feature, you can declare a class containing the specification of the values your query is expected to return, and ask the driver to use that class to spawn new objects from a query result. + + +== Map driver values to a local class + +To map records into objects, define a class having the same attributes as the keys returned by the query. +**The constructor arguments must match exactly the query return keys**, and they are case-sensitive. + +Your class should be defined as a link:https://docs.oracle.com/en/java/javase/17/language/records.html[Java Record], and you provide its definition through the link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/Value.html#as(java.lang.Class)[`Value.as()`] method. + +.Map `:Person` nodes onto a `Person` record class +[source, java] +---- +package demo; + +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.QueryConfig; + +public class App { + + private static final String URI = "neo4j://localhost:7687"; + private static final String USER = "neo4j"; + private static final String PASSWORD = "verysecret"; + + public static void main(String... args) { + try (var driver = GraphDatabase.driver(URI, AuthTokens.basic(USER, PASSWORD))) { + record Person(String name, Integer age) {} + var persons = driver.executableQuery("MERGE (p:Person {name: 'Margarida', age: 29}) RETURN p") + .withConfig(QueryConfig.builder().withDatabase("neo4j").build()) + .execute() + .records() + .stream() + .map(record -> record.get("p").as(Person.class)) + .toList(); + System.out.println(persons.get(0)); // Person[name=Margarida, age=29] + } + } +} +---- + +Declaring the `record` object side-by-side with the query that uses it is a convenient way to obtain results on which it is easy to extract properties. +However, because the class is defined in a local scope, you can't return the mapped values directly: you need to process them in the same function, or manually build another object containing the properties you want to return. +Another solution is to declare the `record` object as a `public` member of the class, or to create a new standalone class containing your `record` definition. +This will make the mapped object available out of the scope of the method in which is was defined. + +[NOTE] +==== +Constructor arguments with generic complex types are not supported. + +[source, java, test-skip] +---- +public record Friends(List names) {} +---- + +Constructor arguments with specific complex types are permitted. +[source, java, test-skip] +---- +public record Friends(List names) {} +---- +==== + + +== Map properties with different names (`@Property`) + +A record's property names and its query return keys can be different. +For example, consider a node `(:Person {name: "Alice"})`. +The returned keys for the query `MERGE (p:Person {name: "Alice"}) RETURN p.name` are `p.name`, even if the property name is `name`. +Similarly, for the query `MERGE (pers:Person {name: "Alice"}) RETURN pers.name`, the return keys are `pers.name`. + +You can always alter the return key with the Cypher operator link:https://neo4j.com/docs/cypher-manual/current/clauses/return/#return-column-alias[`AS`] (ex. `MERGE (p:Person {name: "Alice"}) RETURN p.name AS name`), or use the `@Property()` decorator to specify the property name that the given constructor argument should map to. + +.Properties `name`/`age` are mapped to the object attributes `Name`/`Age` +[source, java] +---- +// import org.neo4j.driver.mapping.Property; + +record Person(@Property("name") String Name, @Property("age") Integer Age) {} +var persons = driver.executableQuery("MERGE (p:Person {name: 'Margarida', age: 29}) RETURN p") + .withConfig(QueryConfig.builder().withDatabase("neo4j").build()) + .execute() + .records() + .stream() + .map(record -> record.get("p").as(Person.class)) + .toList(); +System.out.println(persons.get(0)); // Person[Name=Margarida, Age=29] +---- + + +== Map driver records to a local class + +The earlier examples have mapped a driver value (for example a node identified with `p`) to a class. +You can also use the mapping feature with driver records, through the link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/Record.html#as(java.lang.Class)[`Record.as()`] method. + +.Return keys `name`/`p.age` are mapped to the object attributes `Name`/`Age` +[source, java] +---- +// import org.neo4j.driver.mapping.Property; + +record Person(String name, @Property("p.age") Integer age) {} +var persons = driver.executableQuery(""" + MERGE (p:Person {name: 'Margarida', age: 29}) + RETURN p.name AS name, p.age + """) + .withConfig(QueryConfig.builder().withDatabase("neo4j").build()) + .execute() + .records() + .stream() + .map(record -> record.as(Person.class)) + .toList(); +System.out.println(persons.get(0)); // Person[name=Margarida, age=29] +---- + + +== Work with multiple constructors + +Your class can contain multiple constructors. +In that case, the driver picks one basing on the following criteria (in order of priority): + +- Most matching properties +- Least mis-matching properties + +At least one property must match for a constructor to work with the mapping. + +.An additional constructor to handle the optional `age` property +[source, java] +---- +// import org.neo4j.driver.mapping.Property; + +record Person(String name, int age) { + public Person(@Property("name") String name) { + this(name, -1); + } +} +var persons = driver.executableQuery("MERGE (p:Person {name: 'Axel'}) RETURN p") + .withConfig(QueryConfig.builder().withDatabase("neo4j").build()) + .execute() + .records() + .stream() + .map(record -> record.get("p").as(Person.class)) + .toList(); +---- + +[NOTE] +==== +The compiler renames constructor parameters by default, unless the compiler `-parameters` option is used or the parameters belong to the cannonical constructor of `java.lang.Record`. + +In the example above, the constructor containing only `name` uses the `@Property` annotation even if it doesn't specify a different name than the constructor argument. This is needed because that is not the canonical constructor. +==== + + +== Insert and update data + +You can also use the mapping feature to insert or update data, by creating an instance of the `record` object that serves as a blueprint for your object and then passing it to the query as a parameter. + +.Create and update a `:Person` node +[source, java] +---- +record Person(String name, int age) {} + +var person = new Person("Lucia", 29); +driver.executableQuery("CREATE (:Person $person)") + .withParameters(Map.of("person", person)) + .execute(); + +var happyBirthday = new Person("Lucia", 30); +driver.executableQuery(""" + MATCH (p:Person {name: $person.name}) + SET person += $person + """) + .withParameters(Map.of("person", happyBirthday)) + .execute(); +---- From 7ee437b8cf0aac1ab20eb04948ce0d2087119389 Mon Sep 17 00:00:00 2001 From: Stefano Ottolenghi Date: Tue, 22 Jul 2025 07:40:06 +0200 Subject: [PATCH 2/5] polish --- java-manual/modules/ROOT/content-nav.adoc | 1 + .../modules/ROOT/pages/value-mapping.adoc | 34 ++++++++----------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/java-manual/modules/ROOT/content-nav.adoc b/java-manual/modules/ROOT/content-nav.adoc index 24c53d6b..f1a5c6bf 100644 --- a/java-manual/modules/ROOT/content-nav.adoc +++ b/java-manual/modules/ROOT/content-nav.adoc @@ -5,6 +5,7 @@ * xref:install.adoc[Installation] * xref:connect.adoc[Connect to the database] * xref:query-simple.adoc[Query the database] +* xref:value-mapping.adoc[] * *Advanced usage* diff --git a/java-manual/modules/ROOT/pages/value-mapping.adoc b/java-manual/modules/ROOT/pages/value-mapping.adoc index 24b1883b..7c177433 100644 --- a/java-manual/modules/ROOT/pages/value-mapping.adoc +++ b/java-manual/modules/ROOT/pages/value-mapping.adoc @@ -1,6 +1,6 @@ = Map query results to objects -When xref:query-simple.adoc#read[working with values coming from a query result], you have to manually extract their properties and cast them to the relevant Java types before you can use them. +When xref:query-simple.adoc#read[working with values coming from a query result], you have to manually extract their properties and cast them to the relevant Java types. For example, to retrieve the `name` property as a string, you have to do `person.get("name").asString()`. With the driver's Value Mapping feature, you can declare a class containing the specification of the values your query is expected to return, and ask the driver to use that class to spawn new objects from a query result. @@ -13,7 +13,7 @@ To map records into objects, define a class having the same attributes as the ke Your class should be defined as a link:https://docs.oracle.com/en/java/javase/17/language/records.html[Java Record], and you provide its definition through the link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/Value.html#as(java.lang.Class)[`Value.as()`] method. -.Map `:Person` nodes onto a `Person` record class +.Map `:Person` nodes onto a `Person` record objects [source, java] ---- package demo; @@ -45,25 +45,18 @@ public class App { } ---- +Arguments that don't have a matching property receive a `null` value. +If the argument does not accept a `null` value (for ex. primitive types), an xref:constructors[alternative constructor] that excludes it must be available. +The example above uses the type `Integer` over the primitive `int` to account for nodes missing the `age` property. + Declaring the `record` object side-by-side with the query that uses it is a convenient way to obtain results on which it is easy to extract properties. However, because the class is defined in a local scope, you can't return the mapped values directly: you need to process them in the same function, or manually build another object containing the properties you want to return. Another solution is to declare the `record` object as a `public` member of the class, or to create a new standalone class containing your `record` definition. -This will make the mapped object available out of the scope of the method in which is was defined. +This will make the mapped object available out of the scope of the method in which it was defined. [NOTE] ==== -Constructor arguments with generic complex types are not supported. - -[source, java, test-skip] ----- -public record Friends(List names) {} ----- - -Constructor arguments with specific complex types are permitted. -[source, java, test-skip] ----- -public record Friends(List names) {} ----- +While constructor arguments with specific complex types (ex. `record Friends(List names) {}`) are supported, constructor arguments with generic complex types (ex. `record Friends(List names) {}`) are not supported. ==== @@ -74,14 +67,14 @@ For example, consider a node `(:Person {name: "Alice"})`. The returned keys for the query `MERGE (p:Person {name: "Alice"}) RETURN p.name` are `p.name`, even if the property name is `name`. Similarly, for the query `MERGE (pers:Person {name: "Alice"}) RETURN pers.name`, the return keys are `pers.name`. -You can always alter the return key with the Cypher operator link:https://neo4j.com/docs/cypher-manual/current/clauses/return/#return-column-alias[`AS`] (ex. `MERGE (p:Person {name: "Alice"}) RETURN p.name AS name`), or use the `@Property()` decorator to specify the property name that the given constructor argument should map to. +You can always alter the return key with the Cypher operator link:https://neo4j.com/docs/cypher-manual/current/clauses/return/#return-column-alias[`AS`] (ex. `MERGE (p:Person {name: "Alice"}) RETURN p.name AS name`), or use the `@Property()` decorator to specify the property name that the following constructor argument should map to. -.Properties `name`/`age` are mapped to the object attributes `Name`/`Age` +.Map properties `name`/`age` to the object attributes `firstName`/`Years` [source, java] ---- // import org.neo4j.driver.mapping.Property; -record Person(@Property("name") String Name, @Property("age") Integer Age) {} +record Person(@Property("name") String firstName, @Property("age") Integer Years) {} var persons = driver.executableQuery("MERGE (p:Person {name: 'Margarida', age: 29}) RETURN p") .withConfig(QueryConfig.builder().withDatabase("neo4j").build()) .execute() @@ -89,7 +82,7 @@ var persons = driver.executableQuery("MERGE (p:Person {name: 'Margarida', age: 2 .stream() .map(record -> record.get("p").as(Person.class)) .toList(); -System.out.println(persons.get(0)); // Person[Name=Margarida, Age=29] +System.out.println(persons.get(0)); // Person[firstName=Margarida, Years=29] ---- @@ -118,9 +111,10 @@ System.out.println(persons.get(0)); // Person[name=Margarida, age=29] ---- +[#constructors] == Work with multiple constructors -Your class can contain multiple constructors. +Your Java record class can contain multiple constructors. In that case, the driver picks one basing on the following criteria (in order of priority): - Most matching properties From 4db1f775d47a202a0876bca54080a82844bca56d Mon Sep 17 00:00:00 2001 From: Stefano Ottolenghi Date: Tue, 22 Jul 2025 12:05:48 +0200 Subject: [PATCH 3/5] address review --- .../modules/ROOT/pages/query-simple.adoc | 2 +- .../modules/ROOT/pages/value-mapping.adoc | 21 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/java-manual/modules/ROOT/pages/query-simple.adoc b/java-manual/modules/ROOT/pages/query-simple.adoc index d401441d..28c16d05 100644 --- a/java-manual/modules/ROOT/pages/query-simple.adoc +++ b/java-manual/modules/ROOT/pages/query-simple.adoc @@ -78,7 +78,7 @@ To extract and cast them to the corresponding Java types, use `.as()` (eg. For example, if the `name` property coming from the database is a string, `record.get("name").asString()` will yield the property value as a `String` object. For more information, see xref:data-types.adoc[]. -Another way of extracting values from returned records is by xref:value-mapping.adoc[mapping them objects]. +Another way of extracting values from returned records is by xref:value-mapping.adoc[mapping them to objects]. [#update] diff --git a/java-manual/modules/ROOT/pages/value-mapping.adoc b/java-manual/modules/ROOT/pages/value-mapping.adoc index 7c177433..bcd7865e 100644 --- a/java-manual/modules/ROOT/pages/value-mapping.adoc +++ b/java-manual/modules/ROOT/pages/value-mapping.adoc @@ -1,17 +1,18 @@ = Map query results to objects -When xref:query-simple.adoc#read[working with values coming from a query result], you have to manually extract their properties and cast them to the relevant Java types. +When xref:query-simple.adoc#read[working with values coming from a query result], you have to manually extract their properties and convert them to the relevant Java types. For example, to retrieve the `name` property as a string, you have to do `person.get("name").asString()`. -With the driver's Value Mapping feature, you can declare a class containing the specification of the values your query is expected to return, and ask the driver to use that class to spawn new objects from a query result. +With the driver's Value Mapping feature, you can declare a Java Record containing the specification of the values your query is expected to return, and ask the driver to use that class to spawn new objects from a query result. == Map driver values to a local class -To map records into objects, define a class having the same attributes as the keys returned by the query. +To map records into objects, define a link:https://docs.oracle.com/en/java/javase/17/language/records.html[Java Record] having the same components as the keys returned by the query. **The constructor arguments must match exactly the query return keys**, and they are case-sensitive. -Your class should be defined as a link:https://docs.oracle.com/en/java/javase/17/language/records.html[Java Record], and you provide its definition through the link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/Value.html#as(java.lang.Class)[`Value.as()`] method. +The most straightforward option is to use a link:https://docs.oracle.com/en/java/javase/17/language/records.html[Java Record], but using a standard class with a constructor that matches the query result keys works as well. +Either way, you provide the class definition to the driver through the link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/Value.html#as(java.lang.Class)[`Value.as()`] method. .Map `:Person` nodes onto a `Person` record objects [source, java] @@ -50,7 +51,8 @@ If the argument does not accept a `null` value (for ex. primitive types), an xre The example above uses the type `Integer` over the primitive `int` to account for nodes missing the `age` property. Declaring the `record` object side-by-side with the query that uses it is a convenient way to obtain results on which it is easy to extract properties. -However, because the class is defined in a local scope, you can't return the mapped values directly: you need to process them in the same function, or manually build another object containing the properties you want to return. +However, because the class is defined in a local scope, its type cannot be referenced outside of the method where it is defined. +As a result, you can't return the mapped objects directly: you need to process them in the same function or ensure it implements a type that is accessible outside of the given method. Another solution is to declare the `record` object as a `public` member of the class, or to create a new standalone class containing your `record` definition. This will make the mapped object available out of the scope of the method in which it was defined. @@ -67,7 +69,7 @@ For example, consider a node `(:Person {name: "Alice"})`. The returned keys for the query `MERGE (p:Person {name: "Alice"}) RETURN p.name` are `p.name`, even if the property name is `name`. Similarly, for the query `MERGE (pers:Person {name: "Alice"}) RETURN pers.name`, the return keys are `pers.name`. -You can always alter the return key with the Cypher operator link:https://neo4j.com/docs/cypher-manual/current/clauses/return/#return-column-alias[`AS`] (ex. `MERGE (p:Person {name: "Alice"}) RETURN p.name AS name`), or use the `@Property()` decorator to specify the property name that the following constructor argument should map to. +You can always alter the return key with the Cypher operator link:https://neo4j.com/docs/cypher-manual/current/clauses/return/#return-column-alias[`AS`] (ex. `MERGE (p:Person {name: "Alice"}) RETURN p.name AS name`), or use the `@Property()` annotation to specify the property name that the following constructor argument should map to. .Map properties `name`/`age` to the object attributes `firstName`/`Years` [source, java] @@ -88,8 +90,8 @@ System.out.println(persons.get(0)); // Person[firstName=Margarida, Years=29] == Map driver records to a local class -The earlier examples have mapped a driver value (for example a node identified with `p`) to a class. -You can also use the mapping feature with driver records, through the link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/Record.html#as(java.lang.Class)[`Record.as()`] method. +The earlier examples have mapped a driver `Value` (for example a node identified with `p`) to a class. +You can also use the mapping feature with driver `Record` instances, through the link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/Record.html#as(java.lang.Class)[`Record.as()`] method. .Return keys `name`/`p.age` are mapped to the object attributes `Name`/`Age` [source, java] @@ -149,9 +151,10 @@ In the example above, the constructor containing only `name` uses the `@Property ==== +[#insert-update] == Insert and update data -You can also use the mapping feature to insert or update data, by creating an instance of the `record` object that serves as a blueprint for your object and then passing it to the query as a parameter. +You can also use the mapping feature to insert or update data, by creating an instance of the Java Record object that serves as a blueprint for your object and then passing it to the query as a parameter. .Create and update a `:Person` node [source, java] From 4edd90afd008d837b6538fd2fc01bba46bd513a9 Mon Sep 17 00:00:00 2001 From: Stefano Ottolenghi Date: Tue, 22 Jul 2025 15:56:10 +0200 Subject: [PATCH 4/5] Update java-manual/modules/ROOT/pages/value-mapping.adoc Co-authored-by: Dmitriy Tverdiakov <11927660+injectives@users.noreply.github.com> --- java-manual/modules/ROOT/pages/value-mapping.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-manual/modules/ROOT/pages/value-mapping.adoc b/java-manual/modules/ROOT/pages/value-mapping.adoc index bcd7865e..fd039205 100644 --- a/java-manual/modules/ROOT/pages/value-mapping.adoc +++ b/java-manual/modules/ROOT/pages/value-mapping.adoc @@ -52,7 +52,7 @@ The example above uses the type `Integer` over the primitive `int` to account fo Declaring the `record` object side-by-side with the query that uses it is a convenient way to obtain results on which it is easy to extract properties. However, because the class is defined in a local scope, its type cannot be referenced outside of the method where it is defined. -As a result, you can't return the mapped objects directly: you need to process them in the same function or ensure it implements a type that is accessible outside of the given method. +As a result, such type may not be used as a return type of the method: you need to process the mapped value in the same function or ensure it implements a type that is accessible outside of the given method. Another solution is to declare the `record` object as a `public` member of the class, or to create a new standalone class containing your `record` definition. This will make the mapped object available out of the scope of the method in which it was defined. From db269c169c5c3ff13c86a069cf33a774dcf57094 Mon Sep 17 00:00:00 2001 From: Stefano Ottolenghi Date: Tue, 22 Jul 2025 15:56:20 +0200 Subject: [PATCH 5/5] auth vars --- java-manual/modules/ROOT/pages/value-mapping.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java-manual/modules/ROOT/pages/value-mapping.adoc b/java-manual/modules/ROOT/pages/value-mapping.adoc index fd039205..1e558dd5 100644 --- a/java-manual/modules/ROOT/pages/value-mapping.adoc +++ b/java-manual/modules/ROOT/pages/value-mapping.adoc @@ -26,9 +26,9 @@ import org.neo4j.driver.QueryConfig; public class App { - private static final String URI = "neo4j://localhost:7687"; - private static final String USER = "neo4j"; - private static final String PASSWORD = "verysecret"; + private static final String dbUri = ""; + private static final String dbUser = ""; + private static final String dbPassword = ""; public static void main(String... args) { try (var driver = GraphDatabase.driver(URI, AuthTokens.basic(USER, PASSWORD))) {