From 31c75cbf3012c86120f75e18df5f33033d61456e Mon Sep 17 00:00:00 2001 From: Yunyoung LEE Date: Tue, 16 Jun 2020 21:31:59 +0900 Subject: [PATCH] DATAJDBC-551 - Supports derived delete. --- .../repository/query/AbstractJdbcQuery.java | 2 +- .../query/JdbcDeleteQueryCreator.java | 140 ++++++++++++++++++ .../repository/query/PartTreeJdbcQuery.java | 18 ++- ...sitoryWithCollectionsIntegrationTests.java | 28 +++- 4 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java index 23a50e84858..f91a484739c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java @@ -96,7 +96,7 @@ protected JdbcQueryExecution getQueryExecution(JdbcQueryMethod queryMethod, return extractor != null ? getQueryExecution(extractor) : singleObjectQuery(rowMapper); } - private JdbcQueryExecution createModifyingQueryExecutor() { + protected JdbcQueryExecution createModifyingQueryExecutor() { return (query, parameters) -> { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java new file mode 100644 index 00000000000..05ec7d32bc9 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java @@ -0,0 +1,140 @@ +/* + * 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.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.1 + */ +class JdbcDeleteQueryCreator extends RelationalQueryCreator> { + + 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 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 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 deleteChain, RelationalPersistentEntity entity, Select parentSelect) { + + for (PersistentPropertyPath path : context + .findPersistentPropertyPaths(entity.getType(), p -> true)) { + + PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(context, path); + 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.getIdColumn()).from(sqlContext.getTable()).where(inCondition) + .build(); + deleteRelations(deleteChain, extPath.getLeafEntity(), select); + + deleteChain.add(StatementBuilder.delete(sqlContext.getTable()).where(inCondition).build()); + } + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java index 7215ca4f76d..33680de95ed 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.repository.query; import java.sql.ResultSet; +import java.util.stream.Stream; import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.convert.JdbcConverter; @@ -77,7 +78,8 @@ public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod query ResultSetExtractor extractor = tree.isExistsProjection() ? (ResultSet::next) : null; - this.execution = getQueryExecution(queryMethod, extractor, rowMapper); + this.execution = tree.isDelete() ? createModifyingQueryExecutor() + : getQueryExecution(queryMethod, extractor, rowMapper); } private Sort getDynamicSort(RelationalParameterAccessor accessor) { @@ -94,6 +96,11 @@ public Object execute(Object[] values) { RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(), values); + if (tree.isDelete()) { + return createDeleteQueries(accessor).map(query -> execution.execute(query.getQuery(), query.getParameterSource())) + .reduce((a, b) -> b); + } + ParametrizedQuery query = createQuery(accessor); return this.execution.execute(query.getQuery(), query.getParameterSource()); } @@ -104,4 +111,13 @@ protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor ac JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor); return queryCreator.createQuery(getDynamicSort(accessor)); } + + private Stream createDeleteQueries(RelationalParametersParameterAccessor accessor) { + + RelationalEntityMetadata entityMetadata = getQueryMethod().getEntityInformation(); + JdbcDeleteQueryCreator queryCreator = new JdbcDeleteQueryCreator(context, tree, converter, dialect, entityMetadata, + accessor); + + return queryCreator.createQuery(); + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithCollectionsIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithCollectionsIntegrationTests.java index 5d20f2dc060..a59fc2fbccf 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithCollectionsIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithCollectionsIntegrationTests.java @@ -27,8 +27,6 @@ import java.util.HashSet; import java.util.Set; -import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -45,8 +43,6 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.context.junit4.rules.SpringClassRule; -import org.springframework.test.context.junit4.rules.SpringMethodRule; import org.springframework.transaction.annotation.Transactional; /** @@ -188,6 +184,26 @@ public void deletingWithSet() { 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(); @@ -195,7 +211,9 @@ private Element createElement(String content) { return element; } - interface DummyEntityRepository extends CrudRepository {} + interface DummyEntityRepository extends CrudRepository { + long deleteByName(String name); + } @Configuration @Import(TestConfiguration.class)