Skip to content

Commit 1515a3a

Browse files
nkonevschauder
authored andcommitted
Support derived delete.
Closes #771 See #230 Original pull request #1486
1 parent 721a7ad commit 1515a3a

File tree

6 files changed

+364
-16
lines changed

6 files changed

+364
-16
lines changed
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

+17
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.List;
2424
import java.util.function.Function;
2525
import java.util.function.LongSupplier;
26+
import java.util.stream.Stream;
2627
import java.util.function.Supplier;
2728

2829
import org.springframework.core.convert.converter.Converter;
@@ -127,6 +128,13 @@ public Object execute(Object[] values) {
127128
RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(),
128129
values);
129130

131+
if (tree.isDelete()) {
132+
JdbcQueryExecution<?> execution = createModifyingQueryExecutor();
133+
return createDeleteQueries(accessor)
134+
.map(query -> execution.execute(query.getQuery(), query.getParameterSource()))
135+
.reduce((a, b) -> b);
136+
}
137+
130138
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
131139
ParametrizedQuery query = createQuery(accessor, processor.getReturnedType());
132140
JdbcQueryExecution<?> execution = getQueryExecution(processor, accessor);
@@ -178,6 +186,15 @@ ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor, Re
178186
return queryCreator.createQuery(getDynamicSort(accessor));
179187
}
180188

189+
private Stream<ParametrizedQuery> createDeleteQueries(RelationalParametersParameterAccessor accessor) {
190+
191+
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
192+
193+
JdbcDeleteQueryCreator queryCreator = new JdbcDeleteQueryCreator(context, tree, converter, dialect, entityMetadata,
194+
accessor);
195+
return queryCreator.createQuery();
196+
}
197+
181198
private JdbcQueryExecution<?> getJdbcQueryExecution(@Nullable ResultSetExtractor<Boolean> extractor,
182199
Supplier<RowMapper<?>> rowMapper) {
183200

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

+40-15
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import java.sql.SQLException;
2222
import java.util.ArrayList;
23+
import java.util.Collections;
2324
import java.util.List;
2425

2526
import org.junit.jupiter.api.Test;
@@ -216,28 +217,52 @@ public void deleteAll() {
216217
assertThat(repository.findAll()).isEmpty();
217218
}
218219

219-
private static DummyEntity createDummyEntity() {
220-
DummyEntity entity = new DummyEntity();
221-
entity.setTest("root");
220+
@Test // DATAJDBC-551
221+
public void deleteByTest() {
222222

223-
final Embeddable embeddable = new Embeddable();
224-
embeddable.setTest("embedded");
223+
DummyEntity one = repository.save(createDummyEntity("root1"));
224+
DummyEntity two = repository.save(createDummyEntity("root2"));
225+
DummyEntity three = repository.save(createDummyEntity("root3"));
225226

226-
final DummyEntity2 dummyEntity21 = new DummyEntity2();
227-
dummyEntity21.setTest("entity1");
227+
assertThat(repository.deleteByTest(two.getTest())).isEqualTo(1);
228228

229-
final DummyEntity2 dummyEntity22 = new DummyEntity2();
230-
dummyEntity22.setTest("entity2");
229+
assertThat(repository.findAll()) //
230+
.extracting(DummyEntity::getId) //
231+
.containsExactlyInAnyOrder(one.getId(), three.getId());
231232

232-
embeddable.getList().add(dummyEntity21);
233-
embeddable.getList().add(dummyEntity22);
233+
Long count = template.queryForObject("select count(1) from dummy_entity2", Collections.emptyMap(), Long.class);
234+
assertThat(count).isEqualTo(4);
234235

235-
entity.setEmbeddable(embeddable);
236+
}
236237

237-
return entity;
238-
}
238+
private static DummyEntity createDummyEntity() {
239+
return createDummyEntity("root");
240+
}
241+
242+
private static DummyEntity createDummyEntity(String test) {
243+
DummyEntity entity = new DummyEntity();
244+
entity.setTest(test);
245+
246+
final Embeddable embeddable = new Embeddable();
247+
embeddable.setTest("embedded");
248+
249+
final DummyEntity2 dummyEntity21 = new DummyEntity2();
250+
dummyEntity21.setTest("entity1");
251+
252+
final DummyEntity2 dummyEntity22 = new DummyEntity2();
253+
dummyEntity22.setTest("entity2");
254+
255+
embeddable.getList().add(dummyEntity21);
256+
embeddable.getList().add(dummyEntity22);
257+
258+
entity.setEmbeddable(embeddable);
259+
260+
return entity;
261+
}
239262

240-
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {}
263+
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
264+
int deleteByTest(String test);
265+
}
241266

242267
private static class DummyEntity {
243268
@Column("ID")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package org.springframework.data.jdbc.repository;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS;
5+
6+
import lombok.Data;
7+
import lombok.RequiredArgsConstructor;
8+
9+
import java.util.HashMap;
10+
import java.util.HashSet;
11+
import java.util.Set;
12+
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.ExtendWith;
15+
import org.springframework.beans.factory.annotation.Autowired;
16+
import org.springframework.context.annotation.Bean;
17+
import org.springframework.context.annotation.Configuration;
18+
import org.springframework.context.annotation.Import;
19+
import org.springframework.data.annotation.Id;
20+
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
21+
import org.springframework.data.jdbc.testing.AssumeFeatureTestExecutionListener;
22+
import org.springframework.data.jdbc.testing.TestConfiguration;
23+
import org.springframework.data.repository.CrudRepository;
24+
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
25+
import org.springframework.test.context.ContextConfiguration;
26+
import org.springframework.test.context.TestExecutionListeners;
27+
import org.springframework.test.context.junit.jupiter.SpringExtension;
28+
import org.springframework.transaction.annotation.Transactional;
29+
30+
/**
31+
* Integration tests with collections chain.
32+
*
33+
* @author Yunyoung LEE
34+
*/
35+
@ContextConfiguration
36+
@Transactional
37+
@TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
38+
@ExtendWith(SpringExtension.class)
39+
public class JdbcRepositoryWithCollectionsChainIntegrationTests {
40+
41+
@Autowired NamedParameterJdbcTemplate template;
42+
@Autowired DummyEntityRepository repository;
43+
44+
private static DummyEntity createDummyEntity() {
45+
46+
DummyEntity entity = new DummyEntity();
47+
entity.setName("Entity Name");
48+
return entity;
49+
}
50+
51+
@Test // DATAJDBC-551
52+
public void deleteByName() {
53+
54+
ChildElement element1 = createChildElement("one");
55+
ChildElement element2 = createChildElement("two");
56+
57+
DummyEntity entity = createDummyEntity();
58+
entity.content.add(element1);
59+
entity.content.add(element2);
60+
61+
entity = repository.save(entity);
62+
63+
assertThat(repository.deleteByName("Entity Name")).isEqualTo(1);
64+
65+
assertThat(repository.findById(entity.id)).isEmpty();
66+
67+
Long count = template.queryForObject("select count(1) from grand_child_element", new HashMap<>(), Long.class);
68+
assertThat(count).isEqualTo(0);
69+
}
70+
71+
private ChildElement createChildElement(String name) {
72+
73+
ChildElement element = new ChildElement();
74+
element.name = name;
75+
element.content.add(createGrandChildElement(name + "1"));
76+
element.content.add(createGrandChildElement(name + "2"));
77+
return element;
78+
}
79+
80+
private GrandChildElement createGrandChildElement(String content) {
81+
82+
GrandChildElement element = new GrandChildElement();
83+
element.content = content;
84+
return element;
85+
}
86+
87+
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
88+
long deleteByName(String name);
89+
}
90+
91+
@Configuration
92+
@Import(TestConfiguration.class)
93+
static class Config {
94+
95+
@Autowired JdbcRepositoryFactory factory;
96+
97+
@Bean
98+
Class<?> testClass() {
99+
return JdbcRepositoryWithCollectionsChainIntegrationTests.class;
100+
}
101+
102+
@Bean
103+
DummyEntityRepository dummyEntityRepository() {
104+
return factory.getRepository(DummyEntityRepository.class);
105+
}
106+
}
107+
108+
@Data
109+
static class DummyEntity {
110+
111+
String name;
112+
Set<ChildElement> content = new HashSet<>();
113+
@Id private Long id;
114+
115+
}
116+
117+
@RequiredArgsConstructor
118+
static class ChildElement {
119+
120+
String name;
121+
Set<GrandChildElement> content = new HashSet<>();
122+
@Id private Long id;
123+
}
124+
125+
@RequiredArgsConstructor
126+
static class GrandChildElement {
127+
128+
String content;
129+
@Id private Long id;
130+
}
131+
132+
}

0 commit comments

Comments
 (0)