Skip to content

Commit 1cace88

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 687236b commit 1cace88

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

Lines changed: 1 addition & 1 deletion
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

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

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;
@@ -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

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

Lines changed: 23 additions & 4 deletions
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

Lines changed: 4 additions & 0 deletions
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
}

0 commit comments

Comments
 (0)