Skip to content

Commit b2ee6ca

Browse files
DATAGRAPH-1390 - Provide attribute specific conversions.
This introduces the idea of a `Neo4jPersistentPropertyConverter`, a `Neo4jPersistentPropertyConverterFactory` and an annotation `@ConvertWith`. The annotation can be applied to fields and other annotations. It is used to indicate that a given `Neo4jPersistentProperty` needs a custom conversion for reads and writes and overwrides all existing converters. The annotation allows to specifiy the converter through `converter()` and an optional factory. The default `Neo4jPersistentPropertyConverterFactory` will just try to instantiate the given converter class. More sophisticated solutions need dedicated factories. To make the factories work possible, the whole persistent property is passed as a construction parameter, so that the `@ConvertWith` annotation or any other meta-annotated annotations can be retrieved. In the progress of implementing this, the `Neo4jEntityConverter` has been stripped of its duties to convert single values. Instead, a `Neo4jConversionService` has been introduced that orchestrates the underlying spring service and possible overrides. The conversion will applied at all places where a persistent property can be deduced, either from the model or from parameters targeting the properties. It will not be applied to parameters of string based queries or SpEL.
1 parent 6dda227 commit b2ee6ca

22 files changed

+670
-215
lines changed

etc/jqassistant/structure.adoc

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,17 @@ RETURN p1,p2
2121
.The public support packages must not depend directly on the mapping package
2222
----
2323
MATCH (a:Main:Artifact)
24-
MATCH (a) -[:CONTAINS]-> (p1:Package) -[:DEPENDS_ON]-> (p2:Package) <-[:CONTAINS]- (a)
25-
WHERE p1.fqn in ['org.springframework.data.neo4j.core.convert', 'org.springframework.data.neo4j.core.schema', 'org.springframework.data.neo4j.core.support', 'org.springframework.data.neo4j.core.transaction']
24+
MATCH (a) -[:CONTAINS]-> (p1:Package)
25+
WHERE p1.fqn in [
26+
'org.springframework.data.neo4j.core.convert',
27+
'org.springframework.data.neo4j.core.schema',
28+
'org.springframework.data.neo4j.core.support',
29+
'org.springframework.data.neo4j.core.transaction'
30+
]
31+
WITH p1, a
32+
MATCH (p1) - [:CONTAINS] -> (t:Type)
33+
MATCH (t) - [:DEPENDS_ON] -> (t2:Type) <- [:CONTAINS] - (p2:Package) <-[:CONTAINS]- (a)
34+
WHERE t2.fqn <> 'org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty'
2635
AND p2.fqn = 'org.springframework.data.neo4j.core.mapping'
27-
RETURN p1, p2
36+
RETURN t
2837
----

src/main/asciidoc/appendix/conversions.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ If you require the time zone, use a type that supports it (i.e. `ZoneDateTime`)
177177

178178
== Custom conversions
179179

180+
=== For attributes of a given type
181+
180182
If you prefer to work with your own types in the entities or as parameters for `@Query` annotated methods, you can define and provide a custom converter implementation.
181183
First you have to implement a `GenericConverter` and register the types your converter should handle.
182184
For entity property type converters you need to take care of converting your type to *and* from a Neo4j Java Driver `Value`.
@@ -199,3 +201,12 @@ include::../../../../src/test/java/org/springframework/data/neo4j/documentation/
199201
----
200202

201203
If you need multiple converters in your application, you can add as many as you need in the `Neo4jConversions` constructor.
204+
205+
=== For specific attributes only
206+
207+
If you need conversions only for some specific attributes, we provide `@ConvertWith`.
208+
This is an annotation that can be put on attributes carrying a `Neo4jPersistentPropertyConverter` on it's `converter` attribute
209+
and an optional `Neo4jPersistentPropertyConverterFactory` to construct the former.
210+
With an implementation of `Neo4jPersistentPropertyConverter` all specific conversions for a given type can be addressed.
211+
212+
We provide `@DateLong` and `@DateString` as meta-annotated annotations for backward compatibility with Neo4j-OGM schemes not using native types.

src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@
4949
import org.springframework.data.neo4j.core.mapping.NestedRelationshipContext;
5050
import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingStateMachine;
5151
import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingStateMachine.ProcessState;
52+
import org.springframework.data.neo4j.core.mapping.Constants;
53+
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
5254
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
5355
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
5456
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
55-
import org.springframework.data.neo4j.core.mapping.Constants;
56-
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
5757
import org.springframework.data.neo4j.core.mapping.NodeDescription;
5858
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
5959
import org.springframework.data.neo4j.core.mapping.CreateRelationshipStatementHolder;
@@ -187,31 +187,31 @@ public <T> Optional<T> findOne(String cypherQuery, Map<String, Object> parameter
187187

188188
@Override
189189
public <T> Optional<T> findById(Object id, Class<T> domainType) {
190-
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
190+
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
191191
Statement statement = cypherGenerator
192192
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID)))
193193
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
194194
return createExecutableQuery(domainType, statement, Collections
195-
.singletonMap(Constants.NAME_OF_ID, convertIdValues(id)))
195+
.singletonMap(Constants.NAME_OF_ID, convertIdValues(entityMetaData.getRequiredIdProperty(), id)))
196196
.getSingleResult();
197197
}
198198

199199
@Override
200200
public <T> List<T> findAllById(Iterable<?> ids, Class<T> domainType) {
201-
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
201+
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
202202
Statement statement = cypherGenerator
203203
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().in((parameter(Constants.NAME_OF_IDS))))
204204
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
205205

206206
return createExecutableQuery(domainType, statement, Collections
207-
.singletonMap(Constants.NAME_OF_IDS, convertIdValues(ids)))
207+
.singletonMap(Constants.NAME_OF_IDS, convertIdValues(entityMetaData.getRequiredIdProperty(), ids)))
208208
.getResults();
209209
}
210210

211-
private Object convertIdValues(Object idValues) {
211+
private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, Object idValues) {
212212

213-
return neo4jMappingContext.getEntityConverter().writeValueFromProperty(idValues,
214-
ClassTypeInformation.from(idValues.getClass()));
213+
return neo4jMappingContext.getConversionService().writeValue(idValues,
214+
ClassTypeInformation.from(idValues.getClass()), idProperty == null ? null : idProperty.getOptionalWritingConverter());
215215
}
216216

217217
@Override
@@ -320,14 +320,15 @@ public <T> List<T> saveAll(Iterable<T> instances) {
320320
@Override
321321
public <T> void deleteById(Object id, Class<T> domainType) {
322322

323-
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
323+
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
324324
String nameOfParameter = "id";
325325
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter));
326326

327327
log.debug(() -> String.format("Deleting entity with id %s ", id));
328328

329329
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
330-
ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).in(getDatabaseName()).bind(convertIdValues(id))
330+
ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).in(getDatabaseName())
331+
.bind(convertIdValues(entityMetaData.getRequiredIdProperty(), id))
331332
.to(nameOfParameter).run();
332333

333334
log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(),
@@ -337,14 +338,15 @@ public <T> void deleteById(Object id, Class<T> domainType) {
337338
@Override
338339
public <T> void deleteAllById(Iterable<?> ids, Class<T> domainType) {
339340

340-
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
341+
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
341342
String nameOfParameter = "ids";
342343
Condition condition = entityMetaData.getIdExpression().in(parameter(nameOfParameter));
343344

344345
log.debug(() -> String.format("Deleting all entities with the following ids: %s ", ids));
345346

346347
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
347-
ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).in(getDatabaseName()).bind(convertIdValues(ids))
348+
ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).in(getDatabaseName()).bind(
349+
convertIdValues(entityMetaData.getRequiredIdProperty(), ids))
348350
.to(nameOfParameter).run();
349351

350352
log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(),
@@ -428,7 +430,8 @@ private void processNestedRelations(Neo4jPersistentEntity<?> neo4jPersistentEnti
428430
Statement relationshipRemoveQuery = cypherGenerator.createRelationshipRemoveQuery(neo4jPersistentEntity,
429431
relationshipDescription, previouslyRelatedPersistentEntity);
430432

431-
neo4jClient.query(renderer.render(relationshipRemoveQuery)).in(inDatabase).bind(convertIdValues(fromId))
433+
neo4jClient.query(renderer.render(relationshipRemoveQuery)).in(inDatabase)
434+
.bind(convertIdValues(previouslyRelatedPersistentEntity.getIdProperty(), fromId))
432435
.to(Constants.FROM_ID_PARAMETER_NAME).run();
433436
}
434437

@@ -455,7 +458,8 @@ private void processNestedRelations(Neo4jPersistentEntity<?> neo4jPersistentEnti
455458
neo4jPersistentEntity, relationshipContext, relatedInternalId, relatedValueToStore);
456459

457460
neo4jClient.query(renderer.render(statementHolder.getStatement())).in(inDatabase)
458-
.bind(convertIdValues(fromId)).to(Constants.FROM_ID_PARAMETER_NAME).bindAll(statementHolder.getProperties())
461+
.bind(convertIdValues(targetNodeDescription.getRequiredIdProperty(), fromId))
462+
.to(Constants.FROM_ID_PARAMETER_NAME).bindAll(statementHolder.getProperties())
459463
.run();
460464

461465
// if an internal id is used this must get set to link this entity in the next iteration

src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -182,33 +182,34 @@ public <T> Mono<T> findOne(String cypherQuery, Map<String, Object> parameters, C
182182
@Override
183183
public <T> Mono<T> findById(Object id, Class<T> domainType) {
184184

185-
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
185+
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
186186
Statement statement = cypherGenerator
187187
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID)))
188188
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
189189

190190
return createExecutableQuery(domainType, statement, Collections
191-
.singletonMap(Constants.NAME_OF_ID, convertIdValues(id)))
191+
.singletonMap(Constants.NAME_OF_ID, convertIdValues(entityMetaData.getRequiredIdProperty(), id)))
192192
.flatMap(ExecutableQuery::getSingleResult);
193193
}
194194

195195
@Override
196196
public <T> Flux<T> findAllById(Iterable<?> ids, Class<T> domainType) {
197197

198-
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
198+
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
199199
Statement statement = cypherGenerator
200200
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().in((parameter(Constants.NAME_OF_IDS))))
201201
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
202202

203203
return createExecutableQuery(domainType, statement, Collections
204-
.singletonMap(Constants.NAME_OF_IDS, convertIdValues(ids)))
204+
.singletonMap(Constants.NAME_OF_IDS,
205+
convertIdValues(entityMetaData.getRequiredIdProperty(), ids)))
205206
.flatMapMany(ExecutableQuery::getResults);
206207
}
207208

208-
private Object convertIdValues(Object idValues) {
209+
private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, Object idValues) {
209210

210-
return neo4jMappingContext.getEntityConverter().writeValueFromProperty(idValues,
211-
ClassTypeInformation.from(idValues.getClass()));
211+
return neo4jMappingContext.getConversionService().writeValue(idValues,
212+
ClassTypeInformation.from(idValues.getClass()), idProperty == null ? null : idProperty.getOptionalWritingConverter());
212213
}
213214

214215
@Override
@@ -328,13 +329,15 @@ public <T> Flux<T> saveAll(Iterable<T> instances) {
328329
@Override
329330
public <T> Mono<Void> deleteAllById(Iterable<?> ids, Class<T> domainType) {
330331

331-
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
332+
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
332333
String nameOfParameter = "ids";
333334
Condition condition = entityMetaData.getIdExpression().in(parameter(nameOfParameter));
334335

335336
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
336337
return getDatabaseName().flatMap(databaseName -> this.neo4jClient.query(() -> renderer.render(statement))
337-
.in(databaseName.getValue()).bind(convertIdValues(ids)).to(nameOfParameter).run().then());
338+
.in(databaseName.getValue())
339+
.bind(convertIdValues(entityMetaData.getRequiredIdProperty(), ids))
340+
.to(nameOfParameter).run().then());
338341
}
339342

340343
@Override
@@ -343,12 +346,14 @@ public <T> Mono<Void> deleteById(Object id, Class<T> domainType) {
343346
Assert.notNull(id, "The given id must not be null!");
344347

345348
String nameOfParameter = "id";
346-
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
349+
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
347350
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter));
348351

349352
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
350353
return getDatabaseName().flatMap(databaseName -> this.neo4jClient.query(() -> renderer.render(statement))
351-
.in(databaseName.getValue()).bind(convertIdValues(id)).to(nameOfParameter).run().then());
354+
.in(databaseName.getValue())
355+
.bind(convertIdValues(entityMetaData.getRequiredIdProperty(), id))
356+
.to(nameOfParameter).run().then());
352357
}
353358

354359
@Override
@@ -426,7 +431,8 @@ private Mono<Void> processNestedRelations(Neo4jPersistentEntity<?> neo4jPersiste
426431
Statement relationshipRemoveQuery = cypherGenerator.createRelationshipRemoveQuery(neo4jPersistentEntity,
427432
relationshipDescription, previouslyRelatedPersistentEntity);
428433
relationshipCreationMonos.add(
429-
neo4jClient.query(renderer.render(relationshipRemoveQuery)).in(inDatabase).bind(convertIdValues(fromId))
434+
neo4jClient.query(renderer.render(relationshipRemoveQuery)).in(inDatabase)
435+
.bind(convertIdValues(previouslyRelatedPersistentEntity.getIdProperty(), fromId))
430436
.to(Constants.FROM_ID_PARAMETER_NAME).run().checkpoint("delete relationships").then());
431437
}
432438

@@ -462,7 +468,8 @@ private Mono<Void> processNestedRelations(Neo4jPersistentEntity<?> neo4jPersiste
462468
// in case of no properties the bind will just return an empty map
463469
Mono<ResultSummary> relationshipCreationMonoNested = neo4jClient
464470
.query(renderer.render(statementHolder.getStatement())).in(inDatabase)
465-
.bind(convertIdValues(fromId)).to(Constants.FROM_ID_PARAMETER_NAME)
471+
.bind(convertIdValues(targetNodeDescription.getRequiredIdProperty(), fromId))
472+
.to(Constants.FROM_ID_PARAMETER_NAME)
466473
.bindAll(statementHolder.getProperties()).run();
467474

468475
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2011-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.core.convert;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Inherited;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.apiguardian.api.API;
26+
import org.neo4j.driver.Value;
27+
28+
/**
29+
* This annotation can be used to define either custom conversions for single attributes by specifying a custom
30+
* {@link Neo4jPersistentPropertyConverter} and if needed, a custom factory to create that converter or the annotation
31+
* can be used to build custom meta-annotated annotations like {@code @org.springframework.data.neo4j.core.support.DateLong}.
32+
*
33+
* <p>Custom conversions are applied to both attributes of entities and parameters of repository methods that map to those
34+
* attributes (which does apply to all derived queries and queries by example but not to string based queries).
35+
*
36+
* <p>Converters that have a default constructor don't need a dedicated factory. A dedicated factory will be provided with
37+
* either this annotation and its values or with the meta annotated annotation, including all configuration
38+
* available.
39+
*
40+
* @author Michael J. Simons
41+
* @soundtrack Antilopen Gang - Abwasser
42+
* @since 6.0
43+
*/
44+
@Retention(RetentionPolicy.RUNTIME)
45+
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD })
46+
@Inherited
47+
@Documented
48+
@API(status = API.Status.STABLE, since = "6.0")
49+
public @interface ConvertWith {
50+
51+
Class<? extends Neo4jPersistentPropertyConverter<?>> converter() default UnsetConverter.class;
52+
53+
Class<? extends Neo4jPersistentPropertyConverterFactory> converterFactory() default DefaultNeo4jPersistentPropertyConverterFactory.class;
54+
55+
/**
56+
* Indicates an unset converter.
57+
*/
58+
final class UnsetConverter implements Neo4jPersistentPropertyConverter<Object> {
59+
60+
@Override public Value write(Object source) {
61+
return null;
62+
}
63+
64+
@Override public Object read(Value source) {
65+
return null;
66+
}
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2011-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.core.convert;
17+
18+
import org.springframework.beans.BeanUtils;
19+
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
20+
21+
/**
22+
* @author Michael J. Simons
23+
* @soundtrack Metallica - S&M2
24+
* @since 6.0
25+
*/
26+
final class DefaultNeo4jPersistentPropertyConverterFactory implements Neo4jPersistentPropertyConverterFactory {
27+
28+
@Override
29+
public Neo4jPersistentPropertyConverter getPropertyConverterFor(Neo4jPersistentProperty persistentProperty) {
30+
31+
ConvertWith config = persistentProperty.findAnnotation(ConvertWith.class);
32+
if (config.converter() == ConvertWith.UnsetConverter.class) {
33+
throw new IllegalArgumentException(
34+
"The default custom conversion factory cannot be used with a placeholder");
35+
}
36+
37+
Neo4jPersistentPropertyConverter<?> converter = BeanUtils.instantiateClass(config.converter());
38+
return converter;
39+
}
40+
}

0 commit comments

Comments
 (0)