Skip to content

feat: Introduce Value and Record mapping to custom object types #1633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions driver/clirr-ignored-differences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -633,4 +633,16 @@
<method>org.neo4j.driver.net.ServerAddress of(java.lang.String)</method>
</difference>

<difference>
<className>org/neo4j/driver/Value</className>
<differenceType>7012</differenceType>
<method>java.lang.Object as(java.lang.Class)</method>
</difference>

<difference>
<className>org/neo4j/driver/Record</className>
<differenceType>7012</differenceType>
<method>java.lang.Object as(java.lang.Class)</method>
</difference>

</differences>
1 change: 1 addition & 0 deletions driver/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
exports org.neo4j.driver.util;
exports org.neo4j.driver.exceptions;
exports org.neo4j.driver.exceptions.value;
exports org.neo4j.driver.mapping;

requires org.neo4j.bolt.connection;
requires org.neo4j.bolt.connection.netty;
Expand Down
76 changes: 76 additions & 0 deletions driver/src/main/java/org/neo4j/driver/Record.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@
import java.util.List;
import org.neo4j.driver.exceptions.ClientException;
import org.neo4j.driver.exceptions.NoSuchRecordException;
import org.neo4j.driver.exceptions.value.LossyCoercion;
import org.neo4j.driver.exceptions.value.Uncoercible;
import org.neo4j.driver.mapping.Property;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.MapAccessorWithDefaultValue;
import org.neo4j.driver.util.Immutable;
import org.neo4j.driver.util.Pair;
import org.neo4j.driver.util.Preview;

/**
* Container for Cypher result values.
Expand Down Expand Up @@ -78,4 +83,75 @@ public interface Record extends MapAccessorWithDefaultValue {
* @throws NoSuchRecordException if the associated underlying record is not available
*/
List<Pair<String, Value>> fields();

/**
* Maps values of this record to properties of the given type providing that is it supported.
* <p>
* Example (using the <a href=https://github.com/neo4j-graph-examples/movies>Neo4j Movies Database</a>):
* <pre>
* {@code
* // 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) AS actors")
* .execute()
* .records()
* .stream()
* .map(record -> record.as(MovieInfo.class))
* .toList();
* }
* </pre>
* <p>
* Note that Object Mapping is an alternative to accessing the user-defined values in a {@link MapAccessor}.
* If Object Graph Mapping (OGM) is needed, please use a higher level solution built on top of the driver, like
* <a href="https://neo4j.com/docs/getting-started/languages-guides/java/spring-data-neo4j/">Spring Data Neo4j</a>.
* <p>
* The mapping is done by matching user-defined property names to target type constructor parameters. Therefore, the
* constructor parameters must either have {@link 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 {@code -parameters} option is used or they belong to the cannonical constructor of
* {@link java.lang.Record java.lang.Record}). The name matching is case-sensitive.
* <p>
* Additionally, the {@link Property} annotation may be used when mapping a property with a different name to
* {@link java.lang.Record java.lang.Record} cannonical constructor parameter.
* <p>
* The constructor selection criteria is the following (top priority first):
* <ol>
* <li>Maximum matching properties.</li>
* <li>Minimum mismatching properties.</li>
* </ol>
* The constructor search is done in the order defined by the {@link Class#getDeclaredConstructors} and is
* finished either when a full match is found with no mismatches or once all constructors have been visited.
* <p>
* At least 1 property match must be present for mapping to work.
* <p>
* A {@code null} value is used for arguments that don't have a matching property. If the argument does not accept
* {@code null} value (this includes primitive types), an alternative constructor that excludes it must be
* available.
* <p>
* Types with generic parameters defined at the class level are not supported. However, constructor arguments with
* specific types are permitted, see the {@code actors} parameter in the example above.
* <p>
* On the contrary, the following record would not be mapped because the type information is insufficient:
* <pre>
* {@code
* public record MovieInfo(String title, String director, List<T> actors) {}
* }
* </pre>
* Wildcard type value is not supported.
*
* @param targetClass the target class to map to
* @param <T> the target type to map to
* @return the mapped value
* @throws Uncoercible when mapping to the target type is not possible
* @throws LossyCoercion when mapping cannot be achieved without losing precision
* @see Property
* @see Value#as(Class)
* @since 5.28.5
*/
@Preview(name = "Object mapping")
default <T> T as(Class<T> targetClass) {
// for backwards compatibility only
throw new UnsupportedOperationException("not supported");
}
}
Loading