Skip to content

DATAJDBC-551 - Supports derived delete. #230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ protected JdbcQueryExecution<?> getQueryExecution(JdbcQueryMethod queryMethod,
return extractor != null ? getQueryExecution(extractor) : singleObjectQuery(rowMapper);
}

private JdbcQueryExecution<Object> createModifyingQueryExecutor() {
protected JdbcQueryExecution<Object> createModifyingQueryExecutor() {

return (query, parameters) -> {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* 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.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());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Collection;
import java.util.List;
import java.util.function.LongSupplier;
import java.util.stream.Stream;

import org.springframework.core.convert.converter.Converter;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -123,6 +124,12 @@ public Object execute(Object[] values) {
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);
Expand Down Expand Up @@ -185,6 +192,15 @@ protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor ac
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();
}

/**
* {@link JdbcQueryExecution} returning a {@link org.springframework.data.domain.Slice}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -231,9 +232,31 @@ public void deleteAll() {
assertThat(repository.findAll()).isEmpty();
}

@Test // DATAJDBC-551
public void deleteByTest() {

DummyEntity one = repository.save(createDummyEntity("root1"));
DummyEntity two = repository.save(createDummyEntity("root2"));
DummyEntity three = repository.save(createDummyEntity("root3"));

assertThat(repository.deleteByTest(two.getTest())).isEqualTo(1);

assertThat(repository.findAll()) //
.extracting(DummyEntity::getId) //
.containsExactlyInAnyOrder(one.getId(), three.getId());

Long count = template.queryForObject("select count(1) from dummy_entity2", Collections.emptyMap(), Long.class);
assertThat(count).isEqualTo(4);

}

private static DummyEntity createDummyEntity() {
return createDummyEntity("root");
}

private static DummyEntity createDummyEntity(String test) {
DummyEntity entity = new DummyEntity();
entity.setTest("root");
entity.setTest(test);

final Embeddable embeddable = new Embeddable();
embeddable.setTest("embedded");
Expand All @@ -252,7 +275,9 @@ private static DummyEntity createDummyEntity() {
return entity;
}

interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {}
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
int deleteByTest(String test);
}

@Data
private static class DummyEntity {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Loading