Skip to content

Commit 7bd1833

Browse files
GH-2281 - Support derived deleteBy methods.
This changes adds support for the derived delete by supports. To make this possible, both clients (imperative and reactive) needs to be a bit more lenient about what happens when they receive a `null` on `one`: Technically, when there has been a result but the mapping function was asked to return a `void`, this is correct. Apart from that, the same `detach delete` logic like in `deleteById` is applied. Valid return types for `deleteBy` are `void` or `long` or wrappers. We don’t match and return. This closes #2281.
1 parent efbff8e commit 7bd1833

File tree

13 files changed

+109
-13
lines changed

13 files changed

+109
-13
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ public Optional<T> one() {
281281
try (AutoCloseableQueryRunner statementRunner = getQueryRunner(this.targetDatabase)) {
282282
Result result = runnableStatement.runWith(statementRunner);
283283
Optional<T> optionalValue = result.hasNext() ?
284-
Optional.of(mappingFunction.apply(typeSystem, result.single())) :
284+
Optional.ofNullable(mappingFunction.apply(typeSystem, result.single())) :
285285
Optional.empty();
286286
ResultSummaries.process(result.consume());
287287
return optionalValue;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ Mono<Tuple2<String, Map<String, Object>>> prepareStatement() {
222222
Flux<T> executeWith(Tuple2<String, Map<String, Object>> t, RxQueryRunner runner) {
223223

224224
return Flux.usingWhen(Flux.just(runner.run(t.getT1(), t.getT2())),
225-
result -> Flux.from(result.records()).map(r -> mappingFunction.apply(typeSystem, r)),
225+
result -> Flux.from(result.records()).mapNotNull(r -> mappingFunction.apply(typeSystem, r)),
226226
result -> Flux.from(result.consume()).doOnNext(ResultSummaries::process));
227227
}
228228

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public T apply(TypeSystem typeSystem, Record record) {
5353
}
5454

5555
Value source = record.get(0);
56+
if (targetClass == Void.class || targetClass == void.class) {
57+
return null;
58+
}
5659
return source == null || source == Values.NULL ? null : conversionService.convert(source, targetClass);
5760
}
5861
}

src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.neo4j.cypherdsl.core.Statement;
5050
import org.neo4j.cypherdsl.core.StatementBuilder;
5151
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingMatchAndUpdate;
52+
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingUpdate;
5253
import org.neo4j.cypherdsl.core.SymbolicName;
5354
import org.neo4j.cypherdsl.core.renderer.Renderer;
5455
import org.springframework.data.domain.Sort;
@@ -246,9 +247,18 @@ public Statement prepareDeleteOf(NodeDescription<?> nodeDescription) {
246247

247248
public Statement prepareDeleteOf(NodeDescription<?> nodeDescription, @Nullable Condition condition) {
248249

250+
return prepareDeleteOf(nodeDescription, condition, false);
251+
}
252+
253+
public Statement prepareDeleteOf(NodeDescription<?> nodeDescription, @Nullable Condition condition, boolean count) {
254+
249255
Node rootNode = node(nodeDescription.getPrimaryLabel(), nodeDescription.getAdditionalLabels())
250256
.named(Constants.NAME_OF_ROOT_NODE);
251-
return match(rootNode).where(conditionOrNoCondition(condition)).detachDelete(rootNode).build();
257+
OngoingUpdate ongoingUpdate = match(rootNode).where(conditionOrNoCondition(condition)).detachDelete(rootNode);
258+
if (count) {
259+
return ongoingUpdate.returning(Functions.count(rootNode)).build();
260+
}
261+
return ongoingUpdate.build();
252262
}
253263

254264
public Statement prepareSaveOf(NodeDescription<?> nodeDescription,

src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.neo4j.cypherdsl.core.Property;
4545
import org.neo4j.cypherdsl.core.RelationshipPattern;
4646
import org.neo4j.cypherdsl.core.SortItem;
47+
import org.neo4j.cypherdsl.core.Statement;
4748
import org.neo4j.driver.Value;
4849
import org.neo4j.driver.types.Point;
4950
import org.springframework.data.domain.Pageable;
@@ -56,6 +57,7 @@
5657
import org.springframework.data.mapping.PersistentProperty;
5758
import org.springframework.data.mapping.PersistentPropertyPath;
5859
import org.springframework.data.neo4j.core.mapping.Constants;
60+
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
5961
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
6062
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
6163
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
@@ -256,12 +258,17 @@ protected Condition or(Condition base, Condition condition) {
256258
@Override
257259
protected QueryFragmentsAndParameters complete(@Nullable Condition condition, Sort sort) {
258260

259-
QueryFragmentsAndParameters.QueryFragments queryFragments = createQueryFragments(condition, sort);
260-
261261
Map<String, Object> convertedParameters = this.boundedParameters.stream()
262262
.peek(p -> Neo4jQuerySupport.logParameterIfNull(p.nameOrIndex, p.value))
263263
.collect(Collectors.toMap(p -> p.nameOrIndex, p -> parameterConversion.apply(p.value, p.conversionOverride)));
264-
return new QueryFragmentsAndParameters(nodeDescription, queryFragments, convertedParameters);
264+
265+
if (queryType == Neo4jQueryType.DELETE) {
266+
Statement statement = CypherGenerator.INSTANCE.prepareDeleteOf(nodeDescription, condition, true);
267+
return new QueryFragmentsAndParameters(statement.getCypher(), convertedParameters);
268+
} else {
269+
QueryFragmentsAndParameters.QueryFragments queryFragments = createQueryFragments(condition, sort);
270+
return new QueryFragmentsAndParameters(nodeDescription, queryFragments, convertedParameters);
271+
}
265272
}
266273

267274
@NonNull

src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717

1818
import java.time.Instant;
1919
import java.time.ZoneOffset;
20+
import java.util.Arrays;
2021
import java.util.Collections;
2122
import java.util.HashMap;
23+
import java.util.HashSet;
2224
import java.util.Iterator;
2325
import java.util.List;
2426
import java.util.Map;
27+
import java.util.Set;
2528
import java.util.function.BiFunction;
2629
import java.util.function.Function;
2730
import java.util.function.Supplier;
@@ -63,6 +66,8 @@ abstract class Neo4jQuerySupport {
6366
* The query type.
6467
*/
6568
protected final Neo4jQueryType queryType;
69+
private static final Set<Class<?>> VALID_RETURN_TYPES_FOR_DELETE = Collections.unmodifiableSet(new HashSet<>(
70+
Arrays.asList(Long.class, long.class, Void.class, void.class)));
6671

6772
static final LogAccessor REPOSITORY_QUERY_LOG = new LogAccessor(LogFactory.getLog(Neo4jQuerySupport.class));
6873

@@ -83,6 +88,9 @@ static Class<?> getDomainType(QueryMethod queryMethod) {
8388
Assert.notNull(mappingContext, "The mapping context is required.");
8489
Assert.notNull(queryMethod, "Query method must not be null!");
8590
Assert.notNull(queryType, "Query type must not be null!");
91+
Assert.isTrue(queryType != Neo4jQueryType.DELETE || hasValidReturnTypeForDelete(queryMethod),
92+
"A derived delete query can only return the number of deleted nodes as a long or void."
93+
);
8694

8795
this.mappingContext = mappingContext;
8896
this.queryMethod = queryMethod;
@@ -116,6 +124,10 @@ protected final List<String> getInputProperties(final ResultProcessor resultProc
116124
return returnedType.isProjecting() ? returnedType.getInputProperties() : Collections.emptyList();
117125
}
118126

127+
private static boolean hasValidReturnTypeForDelete(Neo4jQueryMethod queryMethod) {
128+
return VALID_RETURN_TYPES_FOR_DELETE.contains(queryMethod.getResultProcessor().getReturnedType().getReturnedType());
129+
}
130+
119131
static void logParameterIfNull(String name, Object value) {
120132

121133
if (value != null || !REPOSITORY_QUERY_LOG.isDebugEnabled()) {

src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ protected <T extends Object> PreparedQuery<T> prepareQuery(Class<T> returnedType
6767
includedProperties, this::convertParameter, limitModifier);
6868

6969
QueryFragmentsAndParameters queryAndParameters = queryCreator.createQuery();
70-
7170
return PreparedQuery.queryFor(returnedType).withQueryFragmentsAndParameters(queryAndParameters)
7271
.usingMappingFunction(mappingFunction).build();
7372
}

src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,13 @@ public QueryFragmentsAndParameters(NodeDescription<?> nodeDescription, QueryFrag
7272
}
7373

7474
public QueryFragmentsAndParameters(String cypherQuery) {
75+
this(cypherQuery, null);
76+
}
77+
78+
public QueryFragmentsAndParameters(String cypherQuery, Map<String, Object> parameters) {
7579
this.cypherQuery = cypherQuery;
7680
this.queryFragments = new QueryFragments();
77-
this.parameters = null;
81+
this.parameters = parameters;
7882
}
7983

8084
public Map<String, Object> getParameters() {

src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2495,11 +2495,11 @@ class Delete extends IntegrationTestBase {
24952495

24962496
@Override
24972497
void setupData(Transaction transaction) {
2498-
id1 = transaction.run("CREATE (n:PersonWithAllConstructor) RETURN id(n)").next().get(0).asLong();
2499-
id2 = transaction.run("CREATE (n:PersonWithAllConstructor) RETURN id(n)").next().get(0).asLong();
2498+
id1 = transaction.run("CREATE (n:PersonWithAllConstructor {name: $name}) RETURN id(n)", Collections.singletonMap("name", TEST_PERSON1_NAME)).next().get(0).asLong();
2499+
id2 = transaction.run("CREATE (n:PersonWithAllConstructor {name: $name}) RETURN id(n)", Collections.singletonMap("name", TEST_PERSON2_NAME)).next().get(0).asLong();
25002500

2501-
person1 = new PersonWithAllConstructor(id1, null, null, null, null, null, null, null, null, null, null);
2502-
person2 = new PersonWithAllConstructor(id2, null, null, null, null, null, null, null, null, null, null);
2501+
person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, null, null, null, null, null, null, null, null, null);
2502+
person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, null, null, null, null, null, null, null, null, null);
25032503
}
25042504

25052505
@Test
@@ -2520,6 +2520,25 @@ void deleteById(@Autowired PersonRepository repository) {
25202520
assertThat(repository.existsById(id2)).isTrue();
25212521
}
25222522

2523+
@Test // GH-2281
2524+
void deleteByDerivedQuery1(@Autowired PersonRepository repository) {
2525+
2526+
repository.deleteAllByName(TEST_PERSON1_NAME);
2527+
2528+
assertThat(repository.existsById(id1)).isFalse();
2529+
assertThat(repository.existsById(id2)).isTrue();
2530+
}
2531+
2532+
@Test // GH-2281
2533+
void deleteByDerivedQuery2(@Autowired PersonRepository repository) {
2534+
2535+
long deleted = repository.deleteAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME);
2536+
2537+
assertThat(deleted).isEqualTo(2L);
2538+
assertThat(repository.existsById(id1)).isFalse();
2539+
assertThat(repository.existsById(id2)).isFalse();
2540+
}
2541+
25232542
@Test
25242543
void deleteAllEntities(@Autowired PersonRepository repository) {
25252544

src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,4 +305,8 @@ public DtoPersonProjectionContainingAdditionalFields getBySomeLongValue(long val
305305

306306
@Query(value = "MATCH (n:PersonWithAllConstructor) RETURN n :#{ orderBy (#pageable.sort)} SKIP $skip LIMIT $limit")
307307
List<PersonWithAllConstructor> orderBySpel(Pageable page);
308+
309+
void deleteAllByName(String name);
310+
311+
long deleteAllByNameOrName(String name, String otherName);
308312
}

src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2274,6 +2274,29 @@ void deleteCollectionRelationship(@Autowired ReactiveRelationshipRepository repo
22742274
assertThat(person.getPets()).hasSize(1);
22752275
}).verifyComplete();
22762276
}
2277+
2278+
@Test // GH-2281
2279+
void deleteByDerivedQuery1(@Autowired ReactivePersonRepository repository) {
2280+
2281+
repository.deleteAllByName(TEST_PERSON1_NAME)
2282+
.as(StepVerifier::create)
2283+
.verifyComplete();
2284+
2285+
repository.existsById(id1).as(StepVerifier::create).expectNext(false).verifyComplete();
2286+
repository.existsById(id2).as(StepVerifier::create).expectNext(true).verifyComplete();
2287+
}
2288+
2289+
@Test // GH-2281
2290+
void deleteByDerivedQuery2(@Autowired ReactivePersonRepository repository) {
2291+
2292+
repository.deleteAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME)
2293+
.as(StepVerifier::create)
2294+
.expectNext(2L)
2295+
.verifyComplete();
2296+
2297+
repository.existsById(id1).as(StepVerifier::create).expectNext(false).verifyComplete();
2298+
repository.existsById(id2).as(StepVerifier::create).expectNext(false).verifyComplete();
2299+
}
22772300
}
22782301

22792302
@Nested

src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,8 @@ Flux<PersonWithAllConstructor> getOptionalPersonViaQueryWithSort(@Param("part1")
9595

9696
Flux<PersonWithAllConstructor> getOptionalPersonViaNamedQuery(@Param("part1") String part1,
9797
@Param("part2") String part2);
98+
99+
Mono<Void> deleteAllByName(String name);
100+
101+
Mono<Long> deleteAllByNameOrName(String name, String otherName);
98102
}

src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryTest.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ void prepareContext() {
113113
@Test
114114
void validateIgnoreCaseShouldWork() {
115115

116-
117116
assertThatIllegalArgumentException().isThrownBy(() -> repositoryFactory.getRepository(InvalidIgnoreCase.class))
118117
.withMessageMatching("Can not derive query for '.*': Only the case of String based properties can be ignored within the following keywords: \\[IsNotLike, NotLike, IsLike, Like, IsStartingWith, StartingWith, StartsWith, IsEndingWith, EndingWith, EndsWith, IsNotContaining, NotContaining, NotContains, IsContaining, Containing, Contains, IsNot, Not, Is, Equals\\].");
119118
}
@@ -145,6 +144,13 @@ void validateNotACompositePropertyShouldWork() {
145144
assertThatIllegalArgumentException().isThrownBy(() -> repositoryFactory.getRepository(DerivedWithComposite.class))
146145
.withMessageMatching("Can not derive query for '.*': Derived queries are not supported for composite properties.");
147146
}
147+
148+
@Test // GH-2281
149+
void validateDeleteReturnType() {
150+
151+
assertThatIllegalArgumentException().isThrownBy(() -> repositoryFactory.getRepository(InvalidDeleteBy.class))
152+
.withMessageMatching("A derived delete query can only return the number of deleted nodes as a long or void.");
153+
}
148154
}
149155

150156
interface InvalidIgnoreCase extends Neo4jRepository<ThingWithAllAdditionalTypes, Long> {
@@ -167,6 +173,11 @@ interface InvalidSpatial extends Neo4jRepository<ThingWithAllCypherTypes, Long>
167173
Optional<ThingWithAllCypherTypes> findOneByALongIsNear(Point point);
168174
}
169175

176+
interface InvalidDeleteBy extends Neo4jRepository<ThingWithAllCypherTypes, Long> {
177+
178+
Optional<ThingWithAllCypherTypes> deleteAllBy(Point point);
179+
}
180+
170181
interface DerivedWithComposite extends Neo4jRepository<ThingWithCompositeProperties, Long> {
171182

172183
Optional<ThingWithCompositeProperties> findOneByCustomTypeMapTrue();

0 commit comments

Comments
 (0)