Skip to content

Commit 32d49b0

Browse files
committed
feat: Introduce Value and Record mapping to custom object types
Please note that this is a [feature preview](https://github.com/neo4j/neo4j-java-driver/blob/5.0/README.md#preview-features). This update brings support for mapping `org.neo4j.driver.Value` and `org.neo4j.driver.Record` to custom object types. ## org.neo4j.driver.Value The following new method has been introduced to `org.neo4j.driver.Value`: ```java <T> T as(Class<T> targetClass) ``` It maps the value to the given type providing that is it supported. ### Basic Mapping Supported destination types depend on the value `org.neo4j.driver.types.Type`, please see the supported mappings below. - `TypeSystem#BOOLEAN` -> `boolean`, `Boolean` - `TypeSystem#BYTES` -> `byte[]` - `TypeSystem#STRING` -> `String` - `TypeSystem#INTEGER` -> `long`, `Long`, `int`, `Integer`, `double`, `Double`, `float`, `Float` - `TypeSystem#FLOAT` -> `long`, `Long`, `int`, `Integer`, `double`, `Double`, `float`, `Float` - `TypeSystem#PATH` -> `Path` - `TypeSystem#POINT` -> `Point` - `TypeSystem#DATE` -> `LocalDate` - `TypeSystem#TIME` -> `OffsetTime` - `TypeSystem#LOCAL_TIME` -> `LocalTime` - `TypeSystem#LOCAL_DATE_TIME` -> `LocalDateTime` - `TypeSystem#DATE_TIME` -> `ZonedDateTime`, `OffsetDateTime` - `TypeSystem#DURATION` -> `IsoDuration`, `java.time.Period` (only when `seconds = 0` and `nanoseconds = 0` and no overflow happens), `java.time.Duration` (only when `months = 0` and `days = 0` and no overflow happens) - `TypeSystem#NULL` -> `null` - `TypeSystem#LIST` -> `List` - `TypeSystem#MAP` -> `Map` - `TypeSystem#NODE` -> `Node` - `TypeSystem#RELATIONSHIP` -> `Relationship` ### Object Mapping Mapping of user-defined properties to user-defined types is supported for the following value types: - `TypeSystem#NODE` - `TypeSystem#RELATIONSHIP` - `TypeSystem#MAP` Example (using the [Neo4j Movies Database](https://github.com/neo4j-graph-examples/movies)): ```java // assuming the following Java record public record Movie(String title, String tagline, long released) {} // the nodes may be mapped to Movie instances var movies = driver.executableQuery("MATCH (movie:Movie) RETURN movie") .execute() .records() .stream() .map(record -> record.get("movie").as(Movie.class)) .toList(); } ``` Note that Object Mapping is an alternative to accessing the user-defined values in a `org.neo4j.driver.types.MapAccessor`. If Object Graph Mapping (OGM) is needed, please use a higher level solution built on top of the driver, like [Spring Data Neo4j](https://neo4j.com/docs/getting-started/languages-guides/java/spring-data-neo4j/). The mapping is done by matching user-defined property names to target type constructor parameters. Therefore, the constructor parameters must either have `org.neo4j.driver.mapping.Property` annotation or have a matching name that is available at runtime (note that the constructor parameter names are typically changed by the compiler unless either the compiler `-parameters` option is used or they belong to the cannonical constructor of `java.lang.Record`). The name matching is case-sensitive. Additionally, the `org.neo4j.driver.mapping.Property` annotation may be used when mapping a property with a different name to `java.lang.Record` cannonical constructor parameter. The constructor selection criteria is the following (top priority first): - Maximum matching properties. - Minimum mismatching properties. The constructor search is done in the order defined by the `java.lang.Class#getDeclaredConstructors` and is finished either when a full match is found with no mismatches or once all constructors have been visited. At least 1 property match must be present for mapping to work. A `null` value is used for arguments that don't have a matching property. If the argument does not accept `null` value (this includes primitive types), an alternative constructor that excludes it must be available. Example with optional property (using the [Neo4j Movies Database](https://github.com/neo4j-graph-examples/movies)): ```java // assuming the following Java record public record Person(String name, long born) { // alternative constructor for values that don't have 'born' property available public Person(@Property("name") String name) { this(name, -1); } } // the nodes may be mapped to Person instances var persons = driver.executableQuery("MATCH (person:Person) RETURN person") .execute() .records() .stream() .map(record -> record.get("person").as(Person.class)) .toList(); ``` Types with generic parameters defined at the class level are not supported. However, constructor arguments with specific types are permitted. Example (using the [Neo4j Movies Database](https://github.com/neo4j-graph-examples/movies)): ```java // assuming the following Java record public record Acted(List<String> roles) {} // the relationships may be mapped to Acted instances var actedList = driver.executableQuery("MATCH ()-[acted:ACTED_IN]-() RETURN acted") .execute() .records() .stream() .map(record -> record.get("acted").as(Acted.class)) .toList(); ``` On the contrary, the following record would not be mapped because the type information is insufficient: ```java public record Acted<T>(List<T> roles) {} ``` Wildcard type value is not supported. ## org.neo4j.driver.Record The following new method has been introduced to `org.neo4j.driver.Record`: ```java <T> T as(Class<T> targetClass) ``` It maps values of this record to properties of the given type providing that is it supported. Example (using the [Neo4j Movies Database](https://github.com/neo4j-graph-examples/movies)): ```java // assuming the following Java record public record MovieInfo(String title, String director, List<String> actors) {} // the record values may be mapped to MovieInfo instances var movies = driver.executableQuery("MATCH (actor:Person)-[:ACTED_IN]->(movie:Movie)<-[:DIRECTED]-(director:Person) RETURN movie.title as title, director.name AS director, collect(actor.name) Ators") .execute() .records() .stream() .map(record -> record.as(MovieInfo.class)) .toList(); } ``` It follows the same set of rules and has the same requirements as described in the `Object Mapping` `Value` section above.
1 parent 61cd40b commit 32d49b0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1541
-39
lines changed

driver/clirr-ignored-differences.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,4 +633,16 @@
633633
<method>org.neo4j.driver.net.ServerAddress of(java.lang.String)</method>
634634
</difference>
635635

636+
<difference>
637+
<className>org/neo4j/driver/Value</className>
638+
<differenceType>7012</differenceType>
639+
<method>java.lang.Object as(java.lang.Class)</method>
640+
</difference>
641+
642+
<difference>
643+
<className>org/neo4j/driver/Record</className>
644+
<differenceType>7012</differenceType>
645+
<method>java.lang.Object as(java.lang.Class)</method>
646+
</difference>
647+
636648
</differences>

driver/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
exports org.neo4j.driver.util;
3030
exports org.neo4j.driver.exceptions;
3131
exports org.neo4j.driver.exceptions.value;
32+
exports org.neo4j.driver.mapping;
3233

3334
requires org.neo4j.bolt.connection;
3435
requires org.neo4j.bolt.connection.netty;

driver/src/main/java/org/neo4j/driver/Record.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@
1919
import java.util.List;
2020
import org.neo4j.driver.exceptions.ClientException;
2121
import org.neo4j.driver.exceptions.NoSuchRecordException;
22+
import org.neo4j.driver.exceptions.value.LossyCoercion;
23+
import org.neo4j.driver.exceptions.value.Uncoercible;
24+
import org.neo4j.driver.mapping.Property;
25+
import org.neo4j.driver.types.MapAccessor;
2226
import org.neo4j.driver.types.MapAccessorWithDefaultValue;
2327
import org.neo4j.driver.util.Immutable;
2428
import org.neo4j.driver.util.Pair;
29+
import org.neo4j.driver.util.Preview;
2530

2631
/**
2732
* Container for Cypher result values.
@@ -78,4 +83,75 @@ public interface Record extends MapAccessorWithDefaultValue {
7883
* @throws NoSuchRecordException if the associated underlying record is not available
7984
*/
8085
List<Pair<String, Value>> fields();
86+
87+
/**
88+
* Maps values of this record to properties of the given type providing that is it supported.
89+
* <p>
90+
* Example (using the <a href=https://github.com/neo4j-graph-examples/movies>Neo4j Movies Database</a>):
91+
* <pre>
92+
* {@code
93+
* // assuming the following Java record
94+
* public record MovieInfo(String title, String director, List<String> actors) {}
95+
* // the record values may be mapped to MovieInfo instances
96+
* var movies = driver.executableQuery("MATCH (actor:Person)-[:ACTED_IN]->(movie:Movie)<-[:DIRECTED]-(director:Person) RETURN movie.title as title, director.name AS director, collect(actor.name) AS actors")
97+
* .execute()
98+
* .records()
99+
* .stream()
100+
* .map(record -> record.as(MovieInfo.class))
101+
* .toList();
102+
* }
103+
* </pre>
104+
* <p>
105+
* Note that Object Mapping is an alternative to accessing the user-defined values in a {@link MapAccessor}.
106+
* If Object Graph Mapping (OGM) is needed, please use a higher level solution built on top of the driver, like
107+
* <a href="https://neo4j.com/docs/getting-started/languages-guides/java/spring-data-neo4j/">Spring Data Neo4j</a>.
108+
* <p>
109+
* The mapping is done by matching user-defined property names to target type constructor parameters. Therefore, the
110+
* constructor parameters must either have {@link Property} annotation or have a matching name that is available
111+
* at runtime (note that the constructor parameter names are typically changed by the compiler unless either the
112+
* compiler {@code -parameters} option is used or they belong to the cannonical constructor of
113+
* {@link java.lang.Record java.lang.Record}). The name matching is case-sensitive.
114+
* <p>
115+
* Additionally, the {@link Property} annotation may be used when mapping a property with a different name to
116+
* {@link java.lang.Record java.lang.Record} cannonical constructor parameter.
117+
* <p>
118+
* The constructor selection criteria is the following (top priority first):
119+
* <ol>
120+
* <li>Maximum matching properties.</li>
121+
* <li>Minimum mismatching properties.</li>
122+
* </ol>
123+
* The constructor search is done in the order defined by the {@link Class#getDeclaredConstructors} and is
124+
* finished either when a full match is found with no mismatches or once all constructors have been visited.
125+
* <p>
126+
* At least 1 property match must be present for mapping to work.
127+
* <p>
128+
* A {@code null} value is used for arguments that don't have a matching property. If the argument does not accept
129+
* {@code null} value (this includes primitive types), an alternative constructor that excludes it must be
130+
* available.
131+
* <p>
132+
* Types with generic parameters defined at the class level are not supported. However, constructor arguments with
133+
* specific types are permitted, see the {@code actors} parameter in the example above.
134+
* <p>
135+
* On the contrary, the following record would not be mapped because the type information is insufficient:
136+
* <pre>
137+
* {@code
138+
* public record MovieInfo(String title, String director, List<T> actors) {}
139+
* }
140+
* </pre>
141+
* Wildcard type value is not supported.
142+
*
143+
* @param targetClass the target class to map to
144+
* @param <T> the target type to map to
145+
* @return the mapped value
146+
* @throws Uncoercible when mapping to the target type is not possible
147+
* @throws LossyCoercion when mapping cannot be achieved without losing precision
148+
* @see Property
149+
* @see Value#as(Class)
150+
* @since 5.28.5
151+
*/
152+
@Preview(name = "Object mapping")
153+
default <T> T as(Class<T> targetClass) {
154+
// for backwards compatibility only
155+
throw new UnsupportedOperationException("not supported");
156+
}
81157
}

0 commit comments

Comments
 (0)