Skip to content

Commit bc857bd

Browse files
committed
feat: Introduce Mapping To Type Support
<TODO>
1 parent 61cd40b commit bc857bd

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

+1667
-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: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@
1616
*/
1717
package org.neo4j.driver;
1818

19+
import java.time.LocalDate;
20+
import java.time.LocalDateTime;
21+
import java.time.LocalTime;
22+
import java.time.OffsetDateTime;
23+
import java.time.OffsetTime;
24+
import java.time.ZonedDateTime;
1925
import java.util.List;
26+
import java.util.Map;
2027
import org.neo4j.driver.exceptions.ClientException;
2128
import org.neo4j.driver.exceptions.NoSuchRecordException;
29+
import org.neo4j.driver.exceptions.value.LossyCoercion;
30+
import org.neo4j.driver.exceptions.value.Uncoercible;
31+
import org.neo4j.driver.mapping.Property;
32+
import org.neo4j.driver.types.IsoDuration;
2233
import org.neo4j.driver.types.MapAccessorWithDefaultValue;
34+
import org.neo4j.driver.types.Node;
35+
import org.neo4j.driver.types.Path;
36+
import org.neo4j.driver.types.Point;
37+
import org.neo4j.driver.types.Relationship;
38+
import org.neo4j.driver.types.TypeSystem;
2339
import org.neo4j.driver.util.Immutable;
2440
import org.neo4j.driver.util.Pair;
41+
import org.neo4j.driver.util.Preview;
2542

2643
/**
2744
* Container for Cypher result values.
@@ -78,4 +95,206 @@ public interface Record extends MapAccessorWithDefaultValue {
7895
* @throws NoSuchRecordException if the associated underlying record is not available
7996
*/
8097
List<Pair<String, Value>> fields();
98+
99+
/**
100+
* Maps this value to the given type providing that is it supported.
101+
* <p>
102+
* <b>Basic Mapping</b>
103+
* <p>
104+
* Supported destination types depend on value type, please see the table below for more details.
105+
* <table>
106+
* <caption>Supported Mappings</caption>
107+
* <thead>
108+
* <tr>
109+
* <td>Value Type</td>
110+
* <td>Supported Target Types</td>
111+
* </tr>
112+
* </thead>
113+
* <tbody>
114+
* <tr>
115+
* <td>{@link TypeSystem#BOOLEAN}</td>
116+
* <td>{@code boolean}, {@link Boolean}</td>
117+
* </tr>
118+
* <tr>
119+
* <td>{@link TypeSystem#BYTES}</td>
120+
* <td>{@code byte[]}</td>
121+
* </tr>
122+
* <tr>
123+
* <td>{@link TypeSystem#STRING}</td>
124+
* <td>{@link String}</td>
125+
* </tr>
126+
* <tr>
127+
* <td>{@link TypeSystem#INTEGER}</td>
128+
* <td>{@code long}, {@link Long}, {@code int}, {@link Integer}, {@code double}, {@link Double}, {@code float}, {@link Float}</td>
129+
* </tr>
130+
* <tr>
131+
* <td>{@link TypeSystem#FLOAT}</td>
132+
* <td>{@code long}, {@link Long}, {@code int}, {@link Integer}, {@code double}, {@link Double}, {@code float}, {@link Float}</td>
133+
* </tr>
134+
* <tr>
135+
* <td>{@link TypeSystem#PATH}</td>
136+
* <td>{@link Path}</td>
137+
* </tr>
138+
* <tr>
139+
* <td>{@link TypeSystem#POINT}</td>
140+
* <td>{@link Point}</td>
141+
* </tr>
142+
* <tr>
143+
* <td>{@link TypeSystem#DATE}</td>
144+
* <td>{@link LocalDate}</td>
145+
* </tr>
146+
* <tr>
147+
* <td>{@link TypeSystem#TIME}</td>
148+
* <td>{@link OffsetTime}</td>
149+
* </tr>
150+
* <tr>
151+
* <td>{@link TypeSystem#LOCAL_TIME}</td>
152+
* <td>{@link LocalTime}</td>
153+
* </tr>
154+
* <tr>
155+
* <td>{@link TypeSystem#LOCAL_DATE_TIME}</td>
156+
* <td>{@link LocalDateTime}</td>
157+
* </tr>
158+
* <tr>
159+
* <td>{@link TypeSystem#DATE_TIME}</td>
160+
* <td>{@link ZonedDateTime}, {@link OffsetDateTime}</td>
161+
* </tr>
162+
* <tr>
163+
* <td>{@link TypeSystem#DURATION}</td>
164+
* <td>{@link IsoDuration}, {@link java.time.Period} (only when {@code seconds = 0} and {@code nanoseconds = 0} and no overflow happens),
165+
* {@link java.time.Duration} (only when {@code months = 0} and {@code days = 0} and no overflow happens)</td>
166+
* </tr>
167+
* <tr>
168+
* <td>{@link TypeSystem#NULL}</td>
169+
* <td>{@code null}</td>
170+
* </tr>
171+
* <tr>
172+
* <td>{@link TypeSystem#LIST}</td>
173+
* <td>{@link List}</td>
174+
* </tr>
175+
* <tr>
176+
* <td>{@link TypeSystem#MAP}</td>
177+
* <td>{@link Map}</td>
178+
* </tr>
179+
* <tr>
180+
* <td>{@link TypeSystem#NODE}</td>
181+
* <td>{@link Node}</td>
182+
* </tr>
183+
* <tr>
184+
* <td>{@link TypeSystem#RELATIONSHIP}</td>
185+
* <td>{@link Relationship}</td>
186+
* </tr>
187+
* </tbody>
188+
* </table>
189+
*
190+
* <p>
191+
* <b>Object Mapping</b>
192+
* <p>
193+
* Mapping of user-defined properties to user-defined types is supported for the following value types:
194+
* <ul>
195+
* <li>{@link TypeSystem#NODE()}</li>
196+
* <li>{@link TypeSystem#RELATIONSHIP()}</li>
197+
* <li>{@link TypeSystem#MAP()}</li>
198+
* </ul>
199+
* <p>
200+
* Example (using the <a href=https://github.com/neo4j-graph-examples/movies>Neo4j Movies Database</a>):
201+
* <pre>
202+
* {@code
203+
* // assuming the following Java record
204+
* public record Movie(String title, String tagline, long released) {}
205+
* // the nodes may be mapped to Movie instances
206+
* var movies = driver.executableQuery("MATCH (movie:Movie) RETURN movie")
207+
* .execute()
208+
* .records()
209+
* .stream()
210+
* .map(record -> record.get("movie").as(Movie.class))
211+
* .toList();
212+
* }
213+
* </pre>
214+
* <p>
215+
* Note that Object Mapping is an alternative to accessing the user-defined values in a map-like data structure.
216+
* If Object Graph Mapping (OGM) is needed, please use a higher level solution built on top of the driver, like
217+
* <a href="https://neo4j.com/docs/getting-started/languages-guides/java/spring-data-neo4j/">Spring Data Neo4j</a>.
218+
* <p>
219+
* The mapping is done by matching user-defined property names to target type constructor arguments. Therefore, the
220+
* constructor arguments must either have {@link Property} annotation or have a matching name that is available
221+
* at runtime (note that the constructor names are typically changed by the compiler unless either the compiler
222+
* {@code -parameters} option is used or they belong to the cannonical constructor of
223+
* {@link java.lang.Record java.lang.Record}). The name matching is case-sensitive.
224+
* <p>
225+
* Additionally, the {@link Property} annotation may be used when mapping a property with a different name to
226+
* {@link java.lang.Record java.lang.Record} cannonical constructor argument.
227+
* <p>
228+
* The constructor selection criteria is the following (top priority first):
229+
* <ol>
230+
* <li>Maximum matching properties.</li>
231+
* <li>Minimum mismatching properties.</li>
232+
* </ol>
233+
* The constructor search is done in the order defined by the {@link Class#getDeclaredConstructors()} and is
234+
* finished either when a full match is found with no mismatches or once all constructors have been visited.
235+
* <p>
236+
* At least 1 property match must be present for mapping to work.
237+
* <p>
238+
* A {@code null} value is used for arguments that don't have a matching property. If the argument does not accept
239+
* {@code null} value (this includes primitive types), an alternative constructor that excludes it must be
240+
* available.
241+
* <p>
242+
* Example with optional property (using the <a href=https://github.com/neo4j-graph-examples/movies>Neo4j Movies Database</a>):
243+
* <pre>
244+
* {@code
245+
* // assuming the following Java record
246+
* public record Person(String name, long born) {
247+
* // alternative constructor for values that don't have 'born' property available
248+
* public Person(@Property("name") String name) {
249+
* this(name, -1);
250+
* }
251+
* }
252+
* // the nodes may be mapped to Person instances
253+
* var persons = driver.executableQuery("MATCH (person:Person) RETURN person")
254+
* .execute()
255+
* .records()
256+
* .stream()
257+
* .map(record -> record.get("person").as(Person.class))
258+
* .toList();
259+
* }
260+
* </pre>
261+
* <p>
262+
* Types with generic parameters defined at the class level are not supported. However, constructor arguments with
263+
* specific types are permitted.
264+
* <p>
265+
* Example (using the <a href=https://github.com/neo4j-graph-examples/movies>Neo4j Movies Database</a>):
266+
* <pre>
267+
* {@code
268+
* // assuming the following Java record
269+
* public record Acted(List<String> roles) {}
270+
* // the relationships may be mapped to Acted instances
271+
* var actedList = driver.executableQuery("MATCH ()-[acted:ACTED_IN]-() RETURN acted")
272+
* .execute()
273+
* .records()
274+
* .stream()
275+
* .map(record -> record.get("acted").as(Acted.class))
276+
* .toList();
277+
* }
278+
* </pre>
279+
* On the contrary, the following record would not be mapped because type information is insufficient:
280+
* <pre>
281+
* {@code
282+
* public record Acted<T>(List<T> roles) {}
283+
* }
284+
* </pre>
285+
* Wildcard type value is not supported.
286+
*
287+
* @param targetClass the target class to map to
288+
* @param <T> the target type to map to
289+
* @return the mapped value
290+
* @throws Uncoercible when mapping to the target type is not possible
291+
* @throws LossyCoercion when mapping cannot be achieved without losing precision
292+
* @see Property
293+
* @since 5.28.5
294+
*/
295+
@Preview(name = "Object mapping")
296+
default <T> T as(Class<T> targetClass) {
297+
// for backwards compatibility only
298+
throw new UnsupportedOperationException("not supported");
299+
}
81300
}

0 commit comments

Comments
 (0)