Skip to content

Commit a75b59d

Browse files
committed
DATAJDBC-551 - Support derived delete.
1 parent 1683f1d commit a75b59d

File tree

7 files changed

+365
-17
lines changed

7 files changed

+365
-17
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ protected JdbcQueryExecution<?> getQueryExecution(JdbcQueryMethod queryMethod,
9797
return extractor != null ? getQueryExecution(extractor) : singleObjectQuery(rowMapper);
9898
}
9999

100-
private JdbcQueryExecution<Object> createModifyingQueryExecutor() {
100+
JdbcQueryExecution<Object> createModifyingQueryExecutor() {
101101

102102
return (query, parameters) -> {
103103

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright 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.jdbc.repository.query;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.stream.Stream;
21+
22+
import org.springframework.data.domain.Sort;
23+
import org.springframework.data.jdbc.core.convert.JdbcConverter;
24+
import org.springframework.data.jdbc.core.convert.QueryMapper;
25+
import org.springframework.data.mapping.PersistentPropertyPath;
26+
import org.springframework.data.relational.core.dialect.Dialect;
27+
import org.springframework.data.relational.core.dialect.RenderContextFactory;
28+
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
29+
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
30+
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
31+
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
32+
import org.springframework.data.relational.core.query.Criteria;
33+
import org.springframework.data.relational.core.sql.Condition;
34+
import org.springframework.data.relational.core.sql.Conditions;
35+
import org.springframework.data.relational.core.sql.Delete;
36+
import org.springframework.data.relational.core.sql.DeleteBuilder.DeleteWhere;
37+
import org.springframework.data.relational.core.sql.Select;
38+
import org.springframework.data.relational.core.sql.SelectBuilder.SelectWhere;
39+
import org.springframework.data.relational.core.sql.StatementBuilder;
40+
import org.springframework.data.relational.core.sql.Table;
41+
import org.springframework.data.relational.core.sql.render.SqlRenderer;
42+
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
43+
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
44+
import org.springframework.data.relational.repository.query.RelationalQueryCreator;
45+
import org.springframework.data.repository.query.parser.PartTree;
46+
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
47+
import org.springframework.lang.Nullable;
48+
import org.springframework.util.Assert;
49+
50+
/**
51+
* Implementation of {@link RelationalQueryCreator} that creates {@link Stream} of deletion {@link ParametrizedQuery}
52+
* from a {@link PartTree}.
53+
*
54+
* @author Yunyoung LEE
55+
* @since 2.3
56+
*/
57+
class JdbcDeleteQueryCreator extends RelationalQueryCreator<Stream<ParametrizedQuery>> {
58+
59+
private final RelationalMappingContext context;
60+
private final QueryMapper queryMapper;
61+
private final RelationalEntityMetadata<?> entityMetadata;
62+
private final RenderContextFactory renderContextFactory;
63+
64+
/**
65+
* Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect},
66+
* {@link RelationalEntityMetadata} and {@link RelationalParameterAccessor}.
67+
*
68+
* @param context
69+
* @param tree part tree, must not be {@literal null}.
70+
* @param converter must not be {@literal null}.
71+
* @param dialect must not be {@literal null}.
72+
* @param entityMetadata relational entity metadata, must not be {@literal null}.
73+
* @param accessor parameter metadata provider, must not be {@literal null}.
74+
*/
75+
JdbcDeleteQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
76+
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor) {
77+
super(tree, accessor);
78+
79+
Assert.notNull(converter, "JdbcConverter must not be null");
80+
Assert.notNull(dialect, "Dialect must not be null");
81+
Assert.notNull(entityMetadata, "Relational entity metadata must not be null");
82+
83+
this.context = context;
84+
85+
this.entityMetadata = entityMetadata;
86+
this.queryMapper = new QueryMapper(dialect, converter);
87+
this.renderContextFactory = new RenderContextFactory(dialect);
88+
}
89+
90+
@Override
91+
protected Stream<ParametrizedQuery> complete(@Nullable Criteria criteria, Sort sort) {
92+
93+
RelationalPersistentEntity<?> entity = entityMetadata.getTableEntity();
94+
Table table = Table.create(entityMetadata.getTableName());
95+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
96+
97+
SqlContext sqlContext = new SqlContext(entity);
98+
99+
Condition condition = criteria == null ? null
100+
: queryMapper.getMappedObject(parameterSource, criteria, table, entity);
101+
102+
// create select criteria query for subselect
103+
SelectWhere selectBuilder = StatementBuilder.select(sqlContext.getIdColumn()).from(table);
104+
Select select = condition == null ? selectBuilder.build() : selectBuilder.where(condition).build();
105+
106+
// create delete relation queries
107+
List<Delete> deleteChain = new ArrayList<>();
108+
deleteRelations(deleteChain, entity, select);
109+
110+
// crate delete query
111+
DeleteWhere deleteBuilder = StatementBuilder.delete(table);
112+
Delete delete = condition == null ? deleteBuilder.build() : deleteBuilder.where(condition).build();
113+
114+
deleteChain.add(delete);
115+
116+
SqlRenderer renderer = SqlRenderer.create(renderContextFactory.createRenderContext());
117+
return deleteChain.stream().map(d -> new ParametrizedQuery(renderer.render(d), parameterSource));
118+
}
119+
120+
private void deleteRelations(List<Delete> deleteChain, RelationalPersistentEntity<?> entity, Select parentSelect) {
121+
122+
for (PersistentPropertyPath<RelationalPersistentProperty> path : context
123+
.findPersistentPropertyPaths(entity.getType(), p -> true)) {
124+
125+
PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(context, path);
126+
127+
// prevent duplication on recursive call
128+
if (path.getLength() > 1 && !extPath.getParentPath().isEmbedded()) {
129+
continue;
130+
}
131+
132+
if (extPath.isEntity() && !extPath.isEmbedded()) {
133+
134+
SqlContext sqlContext = new SqlContext(extPath.getLeafEntity());
135+
136+
Condition inCondition = Conditions.in(sqlContext.getTable().column(extPath.getReverseColumnName()),
137+
parentSelect);
138+
139+
Select select = StatementBuilder
140+
.select(sqlContext.getTable().column(extPath.getIdDefiningParentPath().getIdColumnName())
141+
// sqlContext.getIdColumn()
142+
).from(sqlContext.getTable()).where(inCondition).build();
143+
deleteRelations(deleteChain, extPath.getLeafEntity(), select);
144+
145+
deleteChain.add(StatementBuilder.delete(sqlContext.getTable()).where(inCondition).build());
146+
}
147+
}
148+
}
149+
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Collection;
2323
import java.util.List;
2424
import java.util.function.LongSupplier;
25+
import java.util.stream.Stream;
2526

2627
import org.springframework.core.convert.converter.Converter;
2728
import org.springframework.data.domain.Pageable;
@@ -120,6 +121,13 @@ public Object execute(Object[] values) {
120121
RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(),
121122
values);
122123

124+
if (tree.isDelete()) {
125+
JdbcQueryExecution<?> execution = createModifyingQueryExecutor();
126+
return createDeleteQueries(accessor)
127+
.map(query -> execution.execute(query.getQuery(), query.getParameterSource()))
128+
.reduce((a, b) -> b);
129+
}
130+
123131
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
124132
ParametrizedQuery query = createQuery(accessor, processor.getReturnedType());
125133
JdbcQueryExecution<?> execution = getQueryExecution(processor, accessor);
@@ -182,6 +190,15 @@ protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor ac
182190
return queryCreator.createQuery(getDynamicSort(accessor));
183191
}
184192

193+
private Stream<ParametrizedQuery> createDeleteQueries(RelationalParametersParameterAccessor accessor) {
194+
195+
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
196+
197+
JdbcDeleteQueryCreator queryCreator = new JdbcDeleteQueryCreator(context, tree, converter, dialect, entityMetadata,
198+
accessor);
199+
return queryCreator.createQuery();
200+
}
201+
185202
/**
186203
* {@link JdbcQueryExecution} returning a {@link org.springframework.data.domain.Slice}.
187204
*

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedWithCollectionIntegrationTests.java

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import java.sql.SQLException;
2424
import java.util.ArrayList;
25+
import java.util.Collections;
2526
import java.util.List;
2627

2728
import org.junit.jupiter.api.Test;
@@ -231,28 +232,52 @@ public void deleteAll() {
231232
assertThat(repository.findAll()).isEmpty();
232233
}
233234

234-
private static DummyEntity createDummyEntity() {
235-
DummyEntity entity = new DummyEntity();
236-
entity.setTest("root");
235+
@Test // DATAJDBC-551
236+
public void deleteByTest() {
237237

238-
final Embeddable embeddable = new Embeddable();
239-
embeddable.setTest("embedded");
238+
DummyEntity one = repository.save(createDummyEntity("root1"));
239+
DummyEntity two = repository.save(createDummyEntity("root2"));
240+
DummyEntity three = repository.save(createDummyEntity("root3"));
240241

241-
final DummyEntity2 dummyEntity21 = new DummyEntity2();
242-
dummyEntity21.setTest("entity1");
242+
assertThat(repository.deleteByTest(two.getTest())).isEqualTo(1);
243243

244-
final DummyEntity2 dummyEntity22 = new DummyEntity2();
245-
dummyEntity22.setTest("entity2");
244+
assertThat(repository.findAll()) //
245+
.extracting(DummyEntity::getId) //
246+
.containsExactlyInAnyOrder(one.getId(), three.getId());
246247

247-
embeddable.getList().add(dummyEntity21);
248-
embeddable.getList().add(dummyEntity22);
248+
Long count = template.queryForObject("select count(1) from dummy_entity2", Collections.emptyMap(), Long.class);
249+
assertThat(count).isEqualTo(4);
249250

250-
entity.setEmbeddable(embeddable);
251+
}
251252

252-
return entity;
253-
}
253+
private static DummyEntity createDummyEntity() {
254+
return createDummyEntity("root");
255+
}
256+
257+
private static DummyEntity createDummyEntity(String test) {
258+
DummyEntity entity = new DummyEntity();
259+
entity.setTest(test);
260+
261+
final Embeddable embeddable = new Embeddable();
262+
embeddable.setTest("embedded");
263+
264+
final DummyEntity2 dummyEntity21 = new DummyEntity2();
265+
dummyEntity21.setTest("entity1");
266+
267+
final DummyEntity2 dummyEntity22 = new DummyEntity2();
268+
dummyEntity22.setTest("entity2");
269+
270+
embeddable.getList().add(dummyEntity21);
271+
embeddable.getList().add(dummyEntity22);
272+
273+
entity.setEmbeddable(embeddable);
274+
275+
return entity;
276+
}
254277

255-
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {}
278+
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
279+
int deleteByTest(String test);
280+
}
256281

257282
@Data
258283
private static class DummyEntity {

0 commit comments

Comments
 (0)