#500 - Example for declarative reactive transactions in R2DBC.

Introduced transactional service using @Transactional on the method declaration. Using the experimental Spring Boot integration to get both an H2ConnectionFactory and R2dbcTransactionManager configured. Added test case to show the rollbacks kicking in on an exception in a map operator.
This commit is contained in:
Oliver Drotbohm
2019-05-06 13:10:18 +02:00
parent c884294bbf
commit 4576dc413f
9 changed files with 167 additions and 46 deletions

View File

@@ -7,3 +7,4 @@ This projects shows some sample usage of the work-in-progress R2DBC support for
- `InfrastructureConfiguration` - sets up a R2DBC `ConnectionFactory` based on the R2DBC Postgres driver (https://github.com/r2dbc/r2dbc-postgresql[r2dbc-postgresql]), a `DatabaseClient` and a `R2dbcRepositoryFactory` to eventually create a `CustomerRepository`.
- `CustomerRepository` - a standard Spring Data reactive CRUD repository exposing query methods using manually defined queries
- `CustomerRepositoryIntegrationTests` - to initialize the database with some setup SQL and the inserting and reading `Customer` instances.
- `TransactionalService` - uses declarative transaction to apply a transactional boundary to repository operations.

View File

@@ -17,7 +17,6 @@ package example.springdata.r2dbc.basics;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Value;
import org.springframework.data.annotation.Id;
@@ -27,6 +26,11 @@ import org.springframework.data.annotation.Id;
@Data
@AllArgsConstructor
class Customer {
@Id Integer id;
String firstname, lastname;
boolean hasId() {
return id != null;
}
}

View File

@@ -26,5 +26,5 @@ import org.springframework.data.repository.reactive.ReactiveCrudRepository;
interface CustomerRepository extends ReactiveCrudRepository<Customer, Long> {
@Query("select id, firstname, lastname from customer c where c.lastname = :lastname")
Flux<Customer> findByLastnameLike(String lastname);
Flux<Customer> findByLastname(String lastname);
}

View File

@@ -0,0 +1,52 @@
/*
* 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
*
* http://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 example.springdata.r2dbc.basics;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Mono;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Oliver Drotbohm
*/
@Component
@RequiredArgsConstructor
class TransactionalService {
private final @NonNull CustomerRepository repository;
/**
* Saves the given {@link Customer} unless its firstname is "Dave".
*
* @param customer must not be {@literal null}.
* @return
*/
@Transactional
public Mono<Customer> save(Customer customer) {
return repository.save(customer).map(it -> {
if (it.firstname.equals("Dave")) {
throw new IllegalStateException();
} else {
return it;
}
});
}
}

View File

@@ -27,7 +27,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.r2dbc.function.DatabaseClient;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.test.context.junit4.SpringRunner;
/**
@@ -81,7 +81,7 @@ public class CustomerRepositoryIntegrationTests {
insertCustomers(dave, carter);
customers.findByLastnameLike("Matthews") //
customers.findByLastname("Matthews") //
.as(StepVerifier::create) //
.assertNext(dave::equals) //
.verifyComplete();

View File

@@ -15,31 +15,11 @@
*/
package example.springdata.r2dbc.basics;
import io.r2dbc.h2.H2ConnectionConfiguration;
import io.r2dbc.h2.H2ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Configuration
@EnableR2dbcRepositories
class InfrastructureConfiguration extends AbstractR2dbcConfiguration {
@Bean
@Override
public H2ConnectionFactory connectionFactory() {
H2ConnectionConfiguration config = H2ConnectionConfiguration.builder() //
.inMemory("test-database2") //
.option("DB_CLOSE_DELAY=-1") //
.build();
return new H2ConnectionFactory(config);
}
}
@SpringBootApplication
class InfrastructureConfiguration { }

View File

@@ -0,0 +1,92 @@
/*
* 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
*
* http://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 example.springdata.r2dbc.basics;
import reactor.core.publisher.Hooks;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.test.context.junit4.SpringRunner;
/**
* Integration tests for {@link TransactionalService}.
*
* @author Oliver Drotbohm
* @soundtrack Shame - Tedeschi Trucks Band (Signs)
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = InfrastructureConfiguration.class)
public class TransactionalServiceIntegrationTests {
@Autowired TransactionalService service;
@Autowired CustomerRepository repository;
@Autowired DatabaseClient database;
@Before
public void setUp() {
Hooks.onOperatorDebug();
List<String> statements = Arrays.asList(//
"DROP TABLE IF EXISTS customer;",
"CREATE TABLE customer ( id SERIAL PRIMARY KEY, firstname VARCHAR(100) NOT NULL, lastname VARCHAR(100) NOT NULL);");
statements.forEach(it -> database.execute() //
.sql(it) //
.fetch() //
.rowsUpdated() //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete());
}
@Test // #500
public void exceptionTriggersRollback() {
service.save(new Customer(null, "Dave", "Matthews")) //
.as(StepVerifier::create) //
.expectError() // Error because of the exception triggered within the service
.verify();
// No data inserted due to rollback
repository.findByLastname("Matthews") //
.as(StepVerifier::create) //
.verifyComplete();
}
@Test // #500
public void insertsDataTransactionally() {
service.save(new Customer(null, "Carter", "Beauford")) //
.as(StepVerifier::create) //
.expectNextMatches(Customer::hasId) //
.verifyComplete();
// Data inserted due to commit
repository.findByLastname("Beauford") //
.as(StepVerifier::create) //
.expectNextMatches(Customer::hasId) //
.verifyComplete();
}
}

View File

@@ -0,0 +1,2 @@
logging.level.org.springframework.transaction=TRACE
logging.level.org.springframework.data.r2dbc.connectionfactory=DEBUG

View File

@@ -21,7 +21,8 @@
</modules>
<properties>
<h2.version>1.4.197</h2.version>
<h2.version>1.4.199</h2.version>
<spring.version>5.2.0.BUILD-SNAPSHOT</spring.version>
</properties>
<dependencyManagement>
@@ -29,7 +30,7 @@
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-bom</artifactId>
<version>Arabba-M7</version>
<version>Arabba-BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -39,14 +40,9 @@
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-r2dbc</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-spi</artifactId>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
<version>0.1.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
@@ -61,8 +57,8 @@
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
@@ -71,11 +67,6 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.addons</groupId>
<artifactId>reactor-extra</artifactId>
</dependency>
</dependencies>
<profiles>
@@ -85,8 +76,7 @@
<profile>
<id>spring-data-next</id>
<properties>
<spring-data-releasetrain.version>Moore-BUILD-SNAPSHOT
</spring-data-releasetrain.version>
<spring-data-releasetrain.version>Moore-BUILD-SNAPSHOT</spring-data-releasetrain.version>
</properties>
</profile>