#235 - Add Modifying query annotation.

We now support returning the affected rows count for repository query methods that are annotated with the Modifying annotation. A modifying query method can return either the affected row count, a boolean value whether at least one row was updated or suppress value emission.

    @Query("UPDATE person SET firstname = :firstname where lastname = :lastname")
    Mono<Integer> setFixedFirstnameFor(String firstname, String lastname);

Original pull request: #238.
This commit is contained in:
Mark Paluch
2019-11-22 14:51:54 +01:00
committed by Jens Schauder
parent 4801a4fbc2
commit 6d37fde4c7
7 changed files with 159 additions and 7 deletions

View File

@@ -1,6 +1,11 @@
[[new-features]]
= New & Noteworthy
[[new-features.1-0-0-RELEASE]]
== What's New in Spring Data R2DBC 1.0.0 RELEASE
* `@Modifying` annotation for query methods to consume affected row count
[[new-features.1-0-0-RC1]]
== What's New in Spring Data R2DBC 1.0.0 RC1

View File

@@ -123,6 +123,32 @@ NOTE: R2DBC repositories do not support query derivation.
NOTE: R2DBC repositories internally bind parameters to placeholders with `Statement.bind(…)` by index.
[[r2dbc.repositories.modifying]]
=== Modifying Queries
The previous sections describe how to declare queries to access a given entity or collection of entities.
You can add custom modifying behavior by using the facilities described in <<repositories.custom-implementations,Custom Implementations for Spring Data Repositories>>.
As this approach is feasible for comprehensive custom functionality, you can modify queries that only need parameter binding by annotating the query method with `@Modifying`, as shown in the following example:
Declaring manipulating queries
====
[source,java]
----
@Query("UPDATE person SET firstname = :firstname where lastname = :lastname")
Mono<Integer> setFixedFirstnameFor(String firstname, String lastname);
----
====
The result of a modifying query can be:
* `Void` to discard update count and await completion
* `Integer` emitting the affected rows count
* `Boolean` to emit whether at least one row was updated
The `@Modifying` annotation is only relevant in combination with the `@Query` annotation.
Derived custom methods do not require this annotation.
:projection-collection: Flux
include::../{spring-data-commons-docs}/repository-projections.adoc[leveloffset=+2]

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2019 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.r2dbc.repository;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates a query method should be considered a modifying query as that changes the way it needs to be executed.
* <p>
* Queries that should be annotated with a {@code @Modifying} annotation include {@code INSERT}, {@code UPDATE},
* {@code DELETE}, and DDL statements. The result of these queries can be consumed as affected row count.
*
* @author Mark Paluch
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
public @interface Modifying {
}

View File

@@ -24,8 +24,8 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.r2dbc.convert.R2dbcConverter;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.core.FetchSpec;
import org.springframework.data.r2dbc.core.DatabaseClient.GenericExecuteSpec;
import org.springframework.data.r2dbc.core.FetchSpec;
import org.springframework.data.r2dbc.repository.query.R2dbcQueryExecution.ResultProcessingConverter;
import org.springframework.data.r2dbc.repository.query.R2dbcQueryExecution.ResultProcessingExecution;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
@@ -108,7 +108,7 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery {
String tableName = method.getEntityInformation().getTableName();
R2dbcQueryExecution execution = getExecution(
R2dbcQueryExecution execution = getExecution(processor.getReturnedType(),
new ResultProcessingConverter(processor, converter.getMappingContext(), instantiators));
return execution.execute(fetchSpec, processor.getReturnedType().getDomainType(), tableName);
@@ -124,14 +124,35 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery {
/**
* Returns the execution instance to use.
*
* @param returnedType must not be {@literal null}.
* @param resultProcessing must not be {@literal null}.
* @return
*/
private R2dbcQueryExecution getExecution(Converter<Object, Object> resultProcessing) {
return new ResultProcessingExecution(getExecutionToWrap(), resultProcessing);
private R2dbcQueryExecution getExecution(ReturnedType returnedType, Converter<Object, Object> resultProcessing) {
return new ResultProcessingExecution(getExecutionToWrap(returnedType), resultProcessing);
}
private R2dbcQueryExecution getExecutionToWrap() {
private R2dbcQueryExecution getExecutionToWrap(ReturnedType returnedType) {
if (method.isModifyingQuery()) {
if (Boolean.class.isAssignableFrom(returnedType.getReturnedType())) {
return (q, t, c) -> q.rowsUpdated().map(integer -> integer > 0);
}
if (Number.class.isAssignableFrom(returnedType.getReturnedType())) {
return (q, t, c) -> q.rowsUpdated().map(integer -> {
return converter.getConversionService().convert(integer, returnedType.getReturnedType());
});
}
if (Void.class.isAssignableFrom(returnedType.getReturnedType())) {
return (q, t, c) -> q.rowsUpdated().then();
}
return (q, t, c) -> q.rowsUpdated();
}
if (method.isCollectionQuery()) {
return (q, t, c) -> q.all();

View File

@@ -28,6 +28,7 @@ import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
@@ -60,6 +61,7 @@ public class R2dbcQueryMethod extends QueryMethod {
private final Method method;
private final MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext;
private final Optional<Query> query;
private final boolean modifying;
private @Nullable RelationalEntityMetadata<?> metadata;
@@ -109,6 +111,7 @@ public class R2dbcQueryMethod extends QueryMethod {
this.method = method;
this.query = Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class));
this.modifying = AnnotatedElementUtils.hasAnnotation(method, Modifying.class);
}
/* (non-Javadoc)
@@ -132,7 +135,7 @@ public class R2dbcQueryMethod extends QueryMethod {
*/
@Override
public boolean isModifyingQuery() {
return super.isModifyingQuery();
return modifying;
}
/*

View File

@@ -18,11 +18,14 @@ package org.springframework.data.r2dbc.repository;
import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import javax.sql.DataSource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
@@ -44,6 +47,8 @@ import org.springframework.test.context.junit4.SpringRunner;
@ContextConfiguration
public class H2R2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests {
@Autowired private H2LegoSetRepository repository;
@Configuration
@EnableR2dbcRepositories(considerNestedRepositories = true,
includeFilters = @Filter(classes = H2LegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE))
@@ -76,6 +81,30 @@ public class H2R2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIn
return H2LegoSetRepository.class;
}
@Test // gh-235
public void shouldReturnUpdateCount() {
shouldInsertNewItems();
repository.updateManual(42).as(StepVerifier::create).expectNext(2L).verifyComplete();
}
@Test // gh-235
public void shouldReturnUpdateSuccess() {
shouldInsertNewItems();
repository.updateManualAndReturnBoolean(42).as(StepVerifier::create).expectNext(true).verifyComplete();
}
@Test // gh-235
public void shouldNotReturnUpdateCount() {
shouldInsertNewItems();
repository.updateManualAndReturnNothing(42).as(StepVerifier::create).verifyComplete();
}
interface H2LegoSetRepository extends LegoSetRepository {
@Override
@@ -93,5 +122,17 @@ public class H2R2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIn
@Override
@Query("SELECT id FROM legoset")
Flux<Integer> findAllIds();
@Query("UPDATE legoset set manual = :manual")
@Modifying
Mono<Long> updateManual(int manual);
@Query("UPDATE legoset set manual = :manual")
@Modifying
Mono<Boolean> updateManualAndReturnBoolean(int manual);
@Query("UPDATE legoset set manual = :manual")
@Modifying
Mono<Void> updateManualAndReturnNothing(int manual);
}
}

View File

@@ -19,17 +19,21 @@ import static org.assertj.core.api.Assertions.*;
import reactor.core.publisher.Mono;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
import org.springframework.data.repository.Repository;
@@ -59,7 +63,15 @@ public class R2dbcQueryMethodUnitTests {
assertThat(metadata.getTableName()).isEqualTo("contact");
}
@Test
@Test // gh-235
public void detectsModifyingQuery() throws Exception {
R2dbcQueryMethod queryMethod = queryMethod(SampleRepository.class, "method");
assertThat(queryMethod.isModifyingQuery()).isTrue();
}
@Test // gh-235
public void detectsTableNameFromRepoTypeIfReturnTypeNotAssignable() throws Exception {
R2dbcQueryMethod queryMethod = queryMethod(SampleRepository.class, "differentTable");
@@ -67,6 +79,7 @@ public class R2dbcQueryMethodUnitTests {
assertThat(metadata.getJavaType()).isAssignableFrom(Address.class);
assertThat(metadata.getTableName()).isEqualTo("contact");
assertThat(queryMethod.isModifyingQuery()).isFalse();
}
@Test(expected = IllegalArgumentException.class)
@@ -126,6 +139,7 @@ public class R2dbcQueryMethodUnitTests {
interface SampleRepository extends Repository<Contact, Long> {
@MyModifyingAnnotation
List<Contact> method();
List<Address> differentTable();
@@ -138,4 +152,10 @@ public class R2dbcQueryMethodUnitTests {
static class Contact {}
static class Address {}
@Retention(RetentionPolicy.RUNTIME)
@Modifying
public @interface MyModifyingAnnotation {
}
}