Skip to content

Commit def45d2

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 371860b commit def45d2

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ public Optional<T> one() {
303303
try (AutoCloseableQueryRunner statementRunner = getQueryRunner(this.targetDatabase)) {
304304
Result result = runnableStatement.runWith(statementRunner);
305305
Optional<T> optionalValue = result.hasNext() ?
306-
Optional.of(mappingFunction.apply(typeSystem, result.single())) :
306+
Optional.ofNullable(mappingFunction.apply(typeSystem, result.single())) :
307307
Optional.empty();
308308
ResultSummaries.process(result.consume());
309309
return optionalValue;

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ Mono<Tuple2<String, Map<String, Object>>> prepareStatement() {
235235
Flux<T> executeWith(Tuple2<String, Map<String, Object>> t, RxQueryRunner runner) {
236236

237237
return Flux.usingWhen(Flux.just(runner.run(t.getT1(), t.getT2())),
238-
result -> Flux.from(result.records()).map(r -> mappingFunction.apply(typeSystem, r)),
238+
result -> Flux.from(result.records()).mapNotNull(r -> mappingFunction.apply(typeSystem, r)),
239239
result -> Flux.from(result.consume()).doOnNext(ResultSummaries::process));
240240
}
241241

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

+3
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

+11-1
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

+10-3
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-
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+
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

+12
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;
@@ -67,6 +70,8 @@ abstract class Neo4jQuerySupport {
6770
* The query type.
6871
*/
6972
protected final Neo4jQueryType queryType;
73+
private static final Set<Class<?>> VALID_RETURN_TYPES_FOR_DELETE = Collections.unmodifiableSet(new HashSet<>(
74+
Arrays.asList(Long.class, long.class, Void.class, void.class)));
7075

7176
static final LogAccessor REPOSITORY_QUERY_LOG = new LogAccessor(LogFactory.getLog(Neo4jQuerySupport.class));
7277

@@ -87,6 +92,9 @@ static Class<?> getDomainType(QueryMethod queryMethod) {
8792
Assert.notNull(mappingContext, "The mapping context is required.");
8893
Assert.notNull(queryMethod, "Query method must not be null!");
8994
Assert.notNull(queryType, "Query type must not be null!");
95+
Assert.isTrue(queryType != Neo4jQueryType.DELETE || hasValidReturnTypeForDelete(queryMethod),
96+
"A derived delete query can only return the number of deleted nodes as a long or void."
97+
);
9098

9199
this.mappingContext = mappingContext;
92100
this.queryMethod = queryMethod;
@@ -119,6 +127,10 @@ protected final List<String> getInputProperties(final ResultProcessor resultProc
119127
return returnedType.isProjecting() ? returnedType.getInputProperties() : Collections.emptyList();
120128
}
121129

130+
private static boolean hasValidReturnTypeForDelete(Neo4jQueryMethod queryMethod) {
131+
return VALID_RETURN_TYPES_FOR_DELETE.contains(queryMethod.getResultProcessor().getReturnedType().getReturnedType());
132+
}
133+
122134
static void logParameterIfNull(String name, Object value) {
123135

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

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

-1
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

+5-1
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,13 @@ public QueryFragmentsAndParameters(NodeDescription<?> nodeDescription, QueryFrag
5858
}
5959

6060
public QueryFragmentsAndParameters(String cypherQuery) {
61+
this(cypherQuery, null);
62+
}
63+
64+
public QueryFragmentsAndParameters(String cypherQuery, Map<String, Object> parameters) {
6165
this.cypherQuery = cypherQuery;
6266
this.queryFragments = new QueryFragments();
63-
this.parameters = null;
67+
this.parameters = parameters;
6468
}
6569

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

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

+23-4
Original file line numberDiff line numberDiff line change
@@ -2458,11 +2458,11 @@ class Delete extends IntegrationTestBase {
24582458

24592459
@Override
24602460
void setupData(Transaction transaction) {
2461-
id1 = transaction.run("CREATE (n:PersonWithAllConstructor) RETURN id(n)").next().get(0).asLong();
2462-
id2 = transaction.run("CREATE (n:PersonWithAllConstructor) RETURN id(n)").next().get(0).asLong();
2461+
id1 = transaction.run("CREATE (n:PersonWithAllConstructor {name: $name}) RETURN id(n)", Collections.singletonMap("name", TEST_PERSON1_NAME)).next().get(0).asLong();
2462+
id2 = transaction.run("CREATE (n:PersonWithAllConstructor {name: $name}) RETURN id(n)", Collections.singletonMap("name", TEST_PERSON2_NAME)).next().get(0).asLong();
24632463

2464-
person1 = new PersonWithAllConstructor(id1, null, null, null, null, null, null, null, null, null, null);
2465-
person2 = new PersonWithAllConstructor(id2, null, null, null, null, null, null, null, null, null, null);
2464+
person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, null, null, null, null, null, null, null, null, null);
2465+
person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, null, null, null, null, null, null, null, null, null);
24662466
}
24672467

24682468
@Test
@@ -2483,6 +2483,25 @@ void deleteById(@Autowired PersonRepository repository) {
24832483
assertThat(repository.existsById(id2)).isTrue();
24842484
}
24852485

2486+
@Test // GH-2281
2487+
void deleteByDerivedQuery1(@Autowired PersonRepository repository) {
2488+
2489+
repository.deleteAllByName(TEST_PERSON1_NAME);
2490+
2491+
assertThat(repository.existsById(id1)).isFalse();
2492+
assertThat(repository.existsById(id2)).isTrue();
2493+
}
2494+
2495+
@Test // GH-2281
2496+
void deleteByDerivedQuery2(@Autowired PersonRepository repository) {
2497+
2498+
long deleted = repository.deleteAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME);
2499+
2500+
assertThat(deleted).isEqualTo(2L);
2501+
assertThat(repository.existsById(id1)).isFalse();
2502+
assertThat(repository.existsById(id2)).isFalse();
2503+
}
2504+
24862505
@Test
24872506
void deleteAllEntities(@Autowired PersonRepository repository) {
24882507

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

+4
Original file line numberDiff line numberDiff line change
@@ -313,4 +313,8 @@ public DtoPersonProjectionContainingAdditionalFields getBySomeLongValue(long val
313313

314314
@Query(value = "MATCH (n:PersonWithAllConstructor) RETURN n :#{ orderBy (#pageable.sort)} SKIP $skip LIMIT $limit")
315315
List<PersonWithAllConstructor> orderBySpel(Pageable page);
316+
317+
void deleteAllByName(String name);
318+
319+
long deleteAllByNameOrName(String name, String otherName);
316320
}

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

+23
Original file line numberDiff line numberDiff line change
@@ -2175,6 +2175,29 @@ void deleteCollectionRelationship(@Autowired ReactiveRelationshipRepository repo
21752175
assertThat(person.getPets()).hasSize(1);
21762176
}).verifyComplete();
21772177
}
2178+
2179+
@Test // GH-2281
2180+
void deleteByDerivedQuery1(@Autowired ReactivePersonRepository repository) {
2181+
2182+
repository.deleteAllByName(TEST_PERSON1_NAME)
2183+
.as(StepVerifier::create)
2184+
.verifyComplete();
2185+
2186+
repository.existsById(id1).as(StepVerifier::create).expectNext(false).verifyComplete();
2187+
repository.existsById(id2).as(StepVerifier::create).expectNext(true).verifyComplete();
2188+
}
2189+
2190+
@Test // GH-2281
2191+
void deleteByDerivedQuery2(@Autowired ReactivePersonRepository repository) {
2192+
2193+
repository.deleteAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME)
2194+
.as(StepVerifier::create)
2195+
.expectNext(2L)
2196+
.verifyComplete();
2197+
2198+
repository.existsById(id1).as(StepVerifier::create).expectNext(false).verifyComplete();
2199+
repository.existsById(id2).as(StepVerifier::create).expectNext(false).verifyComplete();
2200+
}
21782201
}
21792202

21802203
@Nested

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

+4
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,8 @@ Flux<PersonWithAllConstructor> getOptionalPersonViaQueryWithSort(@Param("part1")
100100

101101
Flux<PersonWithAllConstructor> getOptionalPersonViaNamedQuery(@Param("part1") String part1,
102102
@Param("part2") String part2);
103+
104+
Mono<Void> deleteAllByName(String name);
105+
106+
Mono<Long> deleteAllByNameOrName(String name, String otherName);
103107
}

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ void prepareContext() {
114114
@Test
115115
void validateIgnoreCaseShouldWork() {
116116

117-
118117
assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> repositoryFactory.getRepository(InvalidIgnoreCase.class))
119118
.withMessageMatching("Could not create 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\\].");
120119
}
@@ -146,6 +145,13 @@ void validateNotACompositePropertyShouldWork() {
146145
assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> repositoryFactory.getRepository(DerivedWithComposite.class))
147146
.withMessageMatching("Could not create query for .*: Derived queries are not supported for composite properties.");
148147
}
148+
149+
@Test // GH-2281
150+
void validateDeleteReturnType() {
151+
152+
assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> repositoryFactory.getRepository(InvalidDeleteBy.class))
153+
.withMessageMatching("Could not create query for .*: A derived delete query can only return the number of deleted nodes as a long or void.");
154+
}
149155
}
150156

151157
interface InvalidIgnoreCase extends Neo4jRepository<ThingWithAllAdditionalTypes, Long> {
@@ -168,6 +174,11 @@ interface InvalidSpatial extends Neo4jRepository<ThingWithAllCypherTypes, Long>
168174
Optional<ThingWithAllCypherTypes> findOneByALongIsNear(Point point);
169175
}
170176

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

173184
Optional<ThingWithCompositeProperties> findOneByCustomTypeMapTrue();

0 commit comments

Comments
 (0)