Skip to content

Commit c419220

Browse files
committed
DATAJDBC-551 - Supports derived delete.
1 parent 340e8c6 commit c419220

File tree

7 files changed

+350
-4
lines changed

7 files changed

+350
-4
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ protected JdbcQueryExecution<?> getQueryExecution(JdbcQueryMethod queryMethod,
101101
return extractor != null ? getQueryExecution(extractor) : singleObjectQuery(rowMapper);
102102
}
103103

104-
private JdbcQueryExecution<Object> createModifyingQueryExecutor() {
104+
protected JdbcQueryExecution<Object> createModifyingQueryExecutor() {
105105

106106
return (query, parameters) -> {
107107

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

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

+16
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;
@@ -123,6 +124,12 @@ public Object execute(Object[] values) {
123124
RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(),
124125
values);
125126

127+
if (tree.isDelete()) {
128+
JdbcQueryExecution<?> execution = createModifyingQueryExecutor();
129+
return createDeleteQueries(accessor).map(query -> execution.execute(query.getQuery(), query.getParameterSource()))
130+
.reduce((a, b) -> b);
131+
}
132+
126133
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
127134
ParametrizedQuery query = createQuery(accessor, processor.getReturnedType());
128135
JdbcQueryExecution<?> execution = getQueryExecution(processor, accessor);
@@ -185,6 +192,15 @@ protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor ac
185192
return queryCreator.createQuery(getDynamicSort(accessor));
186193
}
187194

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

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

+27-2
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,9 +232,31 @@ public void deleteAll() {
231232
assertThat(repository.findAll()).isEmpty();
232233
}
233234

235+
@Test // DATAJDBC-551
236+
public void deleteByTest() {
237+
238+
DummyEntity one = repository.save(createDummyEntity("root1"));
239+
DummyEntity two = repository.save(createDummyEntity("root2"));
240+
DummyEntity three = repository.save(createDummyEntity("root3"));
241+
242+
assertThat(repository.deleteByTest(two.getTest())).isEqualTo(1);
243+
244+
assertThat(repository.findAll()) //
245+
.extracting(DummyEntity::getId) //
246+
.containsExactlyInAnyOrder(one.getId(), three.getId());
247+
248+
Long count = template.queryForObject("select count(1) from dummy_entity2", Collections.emptyMap(), Long.class);
249+
assertThat(count).isEqualTo(4);
250+
251+
}
252+
234253
private static DummyEntity createDummyEntity() {
254+
return createDummyEntity("root");
255+
}
256+
257+
private static DummyEntity createDummyEntity(String test) {
235258
DummyEntity entity = new DummyEntity();
236-
entity.setTest("root");
259+
entity.setTest(test);
237260

238261
final Embeddable embeddable = new Embeddable();
239262
embeddable.setTest("embedded");
@@ -252,7 +275,9 @@ private static DummyEntity createDummyEntity() {
252275
return entity;
253276
}
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 {
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)