#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:
committed by
Jens Schauder
parent
4801a4fbc2
commit
6d37fde4c7
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user