Support derived delete.
Closes #771 See #230 Original pull request #1486
This commit is contained in:
committed by
Jens Schauder
parent
721a7ad458
commit
1515a3af42
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jdbc.repository.query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jdbc.core.convert.JdbcConverter;
|
||||
import org.springframework.data.jdbc.core.convert.QueryMapper;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.dialect.RenderContextFactory;
|
||||
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.core.query.Criteria;
|
||||
import org.springframework.data.relational.core.sql.Condition;
|
||||
import org.springframework.data.relational.core.sql.Conditions;
|
||||
import org.springframework.data.relational.core.sql.Delete;
|
||||
import org.springframework.data.relational.core.sql.DeleteBuilder.DeleteWhere;
|
||||
import org.springframework.data.relational.core.sql.Select;
|
||||
import org.springframework.data.relational.core.sql.SelectBuilder.SelectWhere;
|
||||
import org.springframework.data.relational.core.sql.StatementBuilder;
|
||||
import org.springframework.data.relational.core.sql.Table;
|
||||
import org.springframework.data.relational.core.sql.render.SqlRenderer;
|
||||
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
|
||||
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
|
||||
import org.springframework.data.relational.repository.query.RelationalQueryCreator;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link RelationalQueryCreator} that creates {@link Stream} of deletion {@link ParametrizedQuery}
|
||||
* from a {@link PartTree}.
|
||||
*
|
||||
* @author Yunyoung LEE
|
||||
* @since 2.3
|
||||
*/
|
||||
class JdbcDeleteQueryCreator extends RelationalQueryCreator<Stream<ParametrizedQuery>> {
|
||||
|
||||
private final RelationalMappingContext context;
|
||||
private final QueryMapper queryMapper;
|
||||
private final RelationalEntityMetadata<?> entityMetadata;
|
||||
private final RenderContextFactory renderContextFactory;
|
||||
|
||||
/**
|
||||
* Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect},
|
||||
* {@link RelationalEntityMetadata} and {@link RelationalParameterAccessor}.
|
||||
*
|
||||
* @param context
|
||||
* @param tree part tree, must not be {@literal null}.
|
||||
* @param converter must not be {@literal null}.
|
||||
* @param dialect must not be {@literal null}.
|
||||
* @param entityMetadata relational entity metadata, must not be {@literal null}.
|
||||
* @param accessor parameter metadata provider, must not be {@literal null}.
|
||||
*/
|
||||
JdbcDeleteQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
|
||||
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor) {
|
||||
super(tree, accessor);
|
||||
|
||||
Assert.notNull(converter, "JdbcConverter must not be null");
|
||||
Assert.notNull(dialect, "Dialect must not be null");
|
||||
Assert.notNull(entityMetadata, "Relational entity metadata must not be null");
|
||||
|
||||
this.context = context;
|
||||
|
||||
this.entityMetadata = entityMetadata;
|
||||
this.queryMapper = new QueryMapper(dialect, converter);
|
||||
this.renderContextFactory = new RenderContextFactory(dialect);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<ParametrizedQuery> complete(@Nullable Criteria criteria, Sort sort) {
|
||||
|
||||
RelationalPersistentEntity<?> entity = entityMetadata.getTableEntity();
|
||||
Table table = Table.create(entityMetadata.getTableName());
|
||||
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
|
||||
|
||||
SqlContext sqlContext = new SqlContext(entity);
|
||||
|
||||
Condition condition = criteria == null ? null
|
||||
: queryMapper.getMappedObject(parameterSource, criteria, table, entity);
|
||||
|
||||
// create select criteria query for subselect
|
||||
SelectWhere selectBuilder = StatementBuilder.select(sqlContext.getIdColumn()).from(table);
|
||||
Select select = condition == null ? selectBuilder.build() : selectBuilder.where(condition).build();
|
||||
|
||||
// create delete relation queries
|
||||
List<Delete> deleteChain = new ArrayList<>();
|
||||
deleteRelations(deleteChain, entity, select);
|
||||
|
||||
// crate delete query
|
||||
DeleteWhere deleteBuilder = StatementBuilder.delete(table);
|
||||
Delete delete = condition == null ? deleteBuilder.build() : deleteBuilder.where(condition).build();
|
||||
|
||||
deleteChain.add(delete);
|
||||
|
||||
SqlRenderer renderer = SqlRenderer.create(renderContextFactory.createRenderContext());
|
||||
return deleteChain.stream().map(d -> new ParametrizedQuery(renderer.render(d), parameterSource));
|
||||
}
|
||||
|
||||
private void deleteRelations(List<Delete> deleteChain, RelationalPersistentEntity<?> entity, Select parentSelect) {
|
||||
|
||||
for (PersistentPropertyPath<RelationalPersistentProperty> path : context
|
||||
.findPersistentPropertyPaths(entity.getType(), p -> true)) {
|
||||
|
||||
PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(context, path);
|
||||
|
||||
// prevent duplication on recursive call
|
||||
if (path.getLength() > 1 && !extPath.getParentPath().isEmbedded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (extPath.isEntity() && !extPath.isEmbedded()) {
|
||||
|
||||
SqlContext sqlContext = new SqlContext(extPath.getLeafEntity());
|
||||
|
||||
Condition inCondition = Conditions.in(sqlContext.getTable().column(extPath.getReverseColumnName()),
|
||||
parentSelect);
|
||||
|
||||
Select select = StatementBuilder
|
||||
.select(sqlContext.getTable().column(extPath.getIdDefiningParentPath().getIdColumnName())
|
||||
// sqlContext.getIdColumn()
|
||||
).from(sqlContext.getTable()).where(inCondition).build();
|
||||
deleteRelations(deleteChain, extPath.getLeafEntity(), select);
|
||||
|
||||
deleteChain.add(StatementBuilder.delete(sqlContext.getTable()).where(inCondition).build());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
@@ -127,6 +128,13 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
|
||||
RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(),
|
||||
values);
|
||||
|
||||
if (tree.isDelete()) {
|
||||
JdbcQueryExecution<?> execution = createModifyingQueryExecutor();
|
||||
return createDeleteQueries(accessor)
|
||||
.map(query -> execution.execute(query.getQuery(), query.getParameterSource()))
|
||||
.reduce((a, b) -> b);
|
||||
}
|
||||
|
||||
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
|
||||
ParametrizedQuery query = createQuery(accessor, processor.getReturnedType());
|
||||
JdbcQueryExecution<?> execution = getQueryExecution(processor, accessor);
|
||||
@@ -178,6 +186,15 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
|
||||
return queryCreator.createQuery(getDynamicSort(accessor));
|
||||
}
|
||||
|
||||
private Stream<ParametrizedQuery> createDeleteQueries(RelationalParametersParameterAccessor accessor) {
|
||||
|
||||
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
|
||||
|
||||
JdbcDeleteQueryCreator queryCreator = new JdbcDeleteQueryCreator(context, tree, converter, dialect, entityMetadata,
|
||||
accessor);
|
||||
return queryCreator.createQuery();
|
||||
}
|
||||
|
||||
private JdbcQueryExecution<?> getJdbcQueryExecution(@Nullable ResultSetExtractor<Boolean> extractor,
|
||||
Supplier<RowMapper<?>> rowMapper) {
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -216,28 +217,52 @@ public class JdbcRepositoryEmbeddedWithCollectionIntegrationTests {
|
||||
assertThat(repository.findAll()).isEmpty();
|
||||
}
|
||||
|
||||
private static DummyEntity createDummyEntity() {
|
||||
DummyEntity entity = new DummyEntity();
|
||||
entity.setTest("root");
|
||||
@Test // DATAJDBC-551
|
||||
public void deleteByTest() {
|
||||
|
||||
final Embeddable embeddable = new Embeddable();
|
||||
embeddable.setTest("embedded");
|
||||
DummyEntity one = repository.save(createDummyEntity("root1"));
|
||||
DummyEntity two = repository.save(createDummyEntity("root2"));
|
||||
DummyEntity three = repository.save(createDummyEntity("root3"));
|
||||
|
||||
final DummyEntity2 dummyEntity21 = new DummyEntity2();
|
||||
dummyEntity21.setTest("entity1");
|
||||
assertThat(repository.deleteByTest(two.getTest())).isEqualTo(1);
|
||||
|
||||
final DummyEntity2 dummyEntity22 = new DummyEntity2();
|
||||
dummyEntity22.setTest("entity2");
|
||||
assertThat(repository.findAll()) //
|
||||
.extracting(DummyEntity::getId) //
|
||||
.containsExactlyInAnyOrder(one.getId(), three.getId());
|
||||
|
||||
embeddable.getList().add(dummyEntity21);
|
||||
embeddable.getList().add(dummyEntity22);
|
||||
Long count = template.queryForObject("select count(1) from dummy_entity2", Collections.emptyMap(), Long.class);
|
||||
assertThat(count).isEqualTo(4);
|
||||
|
||||
entity.setEmbeddable(embeddable);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
private static DummyEntity createDummyEntity() {
|
||||
return createDummyEntity("root");
|
||||
}
|
||||
|
||||
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {}
|
||||
private static DummyEntity createDummyEntity(String test) {
|
||||
DummyEntity entity = new DummyEntity();
|
||||
entity.setTest(test);
|
||||
|
||||
final Embeddable embeddable = new Embeddable();
|
||||
embeddable.setTest("embedded");
|
||||
|
||||
final DummyEntity2 dummyEntity21 = new DummyEntity2();
|
||||
dummyEntity21.setTest("entity1");
|
||||
|
||||
final DummyEntity2 dummyEntity22 = new DummyEntity2();
|
||||
dummyEntity22.setTest("entity2");
|
||||
|
||||
embeddable.getList().add(dummyEntity21);
|
||||
embeddable.getList().add(dummyEntity22);
|
||||
|
||||
entity.setEmbeddable(embeddable);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
|
||||
int deleteByTest(String test);
|
||||
}
|
||||
|
||||
private static class DummyEntity {
|
||||
@Column("ID")
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package org.springframework.data.jdbc.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
|
||||
import org.springframework.data.jdbc.testing.AssumeFeatureTestExecutionListener;
|
||||
import org.springframework.data.jdbc.testing.TestConfiguration;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Integration tests with collections chain.
|
||||
*
|
||||
* @author Yunyoung LEE
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@Transactional
|
||||
@TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
|
||||
@ExtendWith(SpringExtension.class)
|
||||
public class JdbcRepositoryWithCollectionsChainIntegrationTests {
|
||||
|
||||
@Autowired NamedParameterJdbcTemplate template;
|
||||
@Autowired DummyEntityRepository repository;
|
||||
|
||||
private static DummyEntity createDummyEntity() {
|
||||
|
||||
DummyEntity entity = new DummyEntity();
|
||||
entity.setName("Entity Name");
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-551
|
||||
public void deleteByName() {
|
||||
|
||||
ChildElement element1 = createChildElement("one");
|
||||
ChildElement element2 = createChildElement("two");
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.add(element1);
|
||||
entity.content.add(element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
assertThat(repository.deleteByName("Entity Name")).isEqualTo(1);
|
||||
|
||||
assertThat(repository.findById(entity.id)).isEmpty();
|
||||
|
||||
Long count = template.queryForObject("select count(1) from grand_child_element", new HashMap<>(), Long.class);
|
||||
assertThat(count).isEqualTo(0);
|
||||
}
|
||||
|
||||
private ChildElement createChildElement(String name) {
|
||||
|
||||
ChildElement element = new ChildElement();
|
||||
element.name = name;
|
||||
element.content.add(createGrandChildElement(name + "1"));
|
||||
element.content.add(createGrandChildElement(name + "2"));
|
||||
return element;
|
||||
}
|
||||
|
||||
private GrandChildElement createGrandChildElement(String content) {
|
||||
|
||||
GrandChildElement element = new GrandChildElement();
|
||||
element.content = content;
|
||||
return element;
|
||||
}
|
||||
|
||||
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
|
||||
long deleteByName(String name);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(TestConfiguration.class)
|
||||
static class Config {
|
||||
|
||||
@Autowired JdbcRepositoryFactory factory;
|
||||
|
||||
@Bean
|
||||
Class<?> testClass() {
|
||||
return JdbcRepositoryWithCollectionsChainIntegrationTests.class;
|
||||
}
|
||||
|
||||
@Bean
|
||||
DummyEntityRepository dummyEntityRepository() {
|
||||
return factory.getRepository(DummyEntityRepository.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
static class DummyEntity {
|
||||
|
||||
String name;
|
||||
Set<ChildElement> content = new HashSet<>();
|
||||
@Id private Long id;
|
||||
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class ChildElement {
|
||||
|
||||
String name;
|
||||
Set<GrandChildElement> content = new HashSet<>();
|
||||
@Id private Long id;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class GrandChildElement {
|
||||
|
||||
String content;
|
||||
@Id private Long id;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -173,6 +173,26 @@ public class JdbcRepositoryWithCollectionsIntegrationTests {
|
||||
assertThat(count).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-551
|
||||
public void deleteByName() {
|
||||
|
||||
Element element1 = createElement("one");
|
||||
Element element2 = createElement("two");
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.add(element1);
|
||||
entity.content.add(element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
assertThat(repository.deleteByName("Entity Name")).isEqualTo(1);
|
||||
|
||||
assertThat(repository.findById(entity.id)).isEmpty();
|
||||
|
||||
Long count = template.queryForObject("select count(1) from Element", new HashMap<>(), Long.class);
|
||||
assertThat(count).isEqualTo(0);
|
||||
}
|
||||
|
||||
private Element createElement(String content) {
|
||||
|
||||
Element element = new Element();
|
||||
@@ -180,7 +200,9 @@ public class JdbcRepositoryWithCollectionsIntegrationTests {
|
||||
return element;
|
||||
}
|
||||
|
||||
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {}
|
||||
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
|
||||
long deleteByName(String name);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(TestConfiguration.class)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
CREATE TABLE dummy_entity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100));
|
||||
CREATE TABLE child_element (id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, NAME VARCHAR(100), dummy_entity BIGINT);
|
||||
CREATE TABLE grand_child_element (id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, CONTENT VARCHAR(100), child_element BIGINT);
|
||||
Reference in New Issue
Block a user