#2 - Add transactional support.

We now support transaction hosting and transaction management via TransactionalDatabaseClient.

TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);

Flux<Integer> integerFlux = databaseClient.inTransaction(db -> {

	return db.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
			.bind(0, 42055) //
			.bind(1, "SCHAUFELRADBAGGER") //
			.bindNull("$3") //
			.fetch().rowsUpdated();
});
This commit is contained in:
Mark Paluch
2018-06-26 16:26:00 +02:00
parent 853b3fb449
commit 1464df99d4
13 changed files with 1357 additions and 6 deletions

View File

@@ -0,0 +1,181 @@
/*
* Copyright 2018 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 org.springframework.data.r2dbc.function;
import static org.assertj.core.api.Assertions.*;
import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.NoTransactionException;
/**
* Integration tests for {@link TransactionalDatabaseClient}.
*
* @author Mark Paluch
*/
public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport {
private ConnectionFactory connectionFactory;
private JdbcTemplate jdbc;
@Before
public void before() {
Hooks.onOperatorDebug();
connectionFactory = createConnectionFactory();
String tableToCreate = "CREATE TABLE IF NOT EXISTS legoset (\n"
+ " id integer CONSTRAINT id PRIMARY KEY,\n" + " name varchar(255) NOT NULL,\n"
+ " manual integer NULL\n" + ");";
jdbc = createJdbcTemplate(createDataSource());
jdbc.execute(tableToCreate);
jdbc.execute("DELETE FROM legoset");
}
@Test
public void executeInsertInManagedTransaction() {
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
Flux<Integer> integerFlux = databaseClient.inTransaction(db -> {
return db.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
.bind(0, 42055) //
.bind(1, "SCHAUFELRADBAGGER") //
.bindNull("$3") //
.fetch().rowsUpdated();
});
integerFlux.as(StepVerifier::create) //
.expectNext(1) //
.verifyComplete();
assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055);
}
@Test
public void executeInsertInAutoCommitTransaction() {
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
Mono<Integer> integerFlux = databaseClient.execute()
.sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
.bind(0, 42055) //
.bind(1, "SCHAUFELRADBAGGER") //
.bindNull("$3") //
.fetch().rowsUpdated();
integerFlux.as(StepVerifier::create) //
.expectNext(1) //
.verifyComplete();
assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055);
}
@Test
public void shouldManageUserTransaction() {
Queue<Long> transactionIds = new ArrayBlockingQueue<>(5);
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
Flux<Long> txId = databaseClient.execute().sql("SELECT txid_current();").exchange()
.flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all());
Mono<Void> then = databaseClient.enableTransactionSynchronization(databaseClient.beginTransaction() //
.thenMany(txId.concatWith(txId).doOnNext(transactionIds::add)) //
.then(databaseClient.rollbackTransaction()));
then.as(StepVerifier::create) //
.verifyComplete();
List<Long> listOfTxIds = new ArrayList<>(transactionIds);
assertThat(listOfTxIds).hasSize(2);
assertThat(listOfTxIds).containsExactly(listOfTxIds.get(1), listOfTxIds.get(0));
}
@Test
public void userTransactionManagementShouldFailWithoutSynchronizer() {
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
Mono<Void> then = databaseClient.beginTransaction().then(databaseClient.rollbackTransaction());
then.as(StepVerifier::create) //
.consumeErrorWith(exception -> {
assertThat(exception).isInstanceOf(NoTransactionException.class)
.hasMessageContaining("Transaction management is not enabled");
}).verify();
}
@Test
public void shouldRollbackTransaction() {
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
Flux<Integer> integerFlux = databaseClient.inTransaction(db -> {
return db.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
.bind(0, 42055) //
.bind(1, "SCHAUFELRADBAGGER") //
.bindNull("$3") //
.fetch().rowsUpdated().then(Mono.error(new IllegalStateException("failed")));
});
integerFlux.as(StepVerifier::create) //
.expectError(IllegalStateException.class) //
.verify();
assertThat(jdbc.queryForMap("SELECT count(*) FROM legoset")).containsEntry("count", 0L);
}
@Test
public void emitTransactionIds() {
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
Flux<Long> transactionIds = databaseClient.inTransaction(db -> {
Flux<Long> txId = db.execute().sql("SELECT txid_current();").exchange()
.flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all());
return txId.concatWith(txId);
});
transactionIds.collectList().as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual).hasSize(2);
assertThat(actual).containsExactly(actual.get(1), actual.get(0));
}) //
.verifyComplete();
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2018 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 org.springframework.data.r2dbc.function.connectionfactory;
import static org.mockito.Mockito.*;
import io.r2dbc.spi.ConnectionFactory;
import reactor.test.StepVerifier;
import org.junit.Test;
import org.springframework.transaction.NoTransactionException;
/**
* Unit tests for {@link ConnectionFactoryUtils}.
*
* @author Mark Paluch
*/
public class ConnectionFactoryUtilsUnitTests {
@Test
public void currentReactiveTransactionSynchronizationShouldReportSynchronization() {
ConnectionFactoryUtils.currentReactiveTransactionSynchronization() //
.subscriberContext(
it -> it.put(ReactiveTransactionSynchronization.class, new ReactiveTransactionSynchronization()))
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
}
@Test
public void currentReactiveTransactionSynchronizationShouldFailWithoutTxMgmt() {
ConnectionFactoryUtils.currentReactiveTransactionSynchronization() //
.as(StepVerifier::create) //
.expectError(NoTransactionException.class) //
.verify();
}
@Test
public void currentActiveReactiveTransactionSynchronizationShouldReportSynchronization() {
ConnectionFactoryUtils.currentActiveReactiveTransactionSynchronization() //
.subscriberContext(it -> {
ReactiveTransactionSynchronization sync = new ReactiveTransactionSynchronization();
sync.registerTransaction(TransactionResources.create());
return it.put(ReactiveTransactionSynchronization.class, sync);
}).as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
}
@Test
public void currentActiveReactiveTransactionSynchronization() {
ConnectionFactoryUtils.currentActiveReactiveTransactionSynchronization() //
.subscriberContext(
it -> it.put(ReactiveTransactionSynchronization.class, new ReactiveTransactionSynchronization()))
.as(StepVerifier::create) //
.expectError(NoTransactionException.class) //
.verify();
}
@Test
public void currentConnectionFactoryShouldReportConnectionFactory() {
ConnectionFactory factoryMock = mock(ConnectionFactory.class);
ConnectionFactoryUtils.currentConnectionFactory() //
.subscriberContext(it -> {
ReactiveTransactionSynchronization sync = new ReactiveTransactionSynchronization();
TransactionResources resources = TransactionResources.create();
resources.registerResource(ConnectionFactory.class, factoryMock);
sync.registerTransaction(resources);
return it.put(ReactiveTransactionSynchronization.class, sync);
}).as(StepVerifier::create) //
.expectNext(factoryMock) //
.verifyComplete();
}
}

View File

@@ -27,6 +27,8 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
@@ -36,6 +38,7 @@ import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport;
import org.springframework.data.r2dbc.function.DatabaseClient;
import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.function.TransactionalDatabaseClient;
import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.Table;
@@ -129,6 +132,34 @@ public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport
}).verifyComplete();
}
@Test
public void shouldInsertItemsTransactional() {
TransactionalDatabaseClient client = TransactionalDatabaseClient.builder().connectionFactory(connectionFactory)
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build();
LegoSetRepository transactionalRepository = new R2dbcRepositoryFactory(client, mappingContext)
.getRepository(LegoSetRepository.class);
LegoSet legoSet1 = new LegoSet(null, "SCHAUFELRADBAGGER", 12);
LegoSet legoSet2 = new LegoSet(null, "FORSCHUNGSSCHIFF", 13);
Flux<Map<String, Object>> transactional = client.inTransaction(db -> {
return transactionalRepository.save(legoSet1) //
.map(it -> jdbc.queryForMap("SELECT count(*) FROM repo_legoset"));
});
Mono<Map<String, Object>> nonTransactional = transactionalRepository.save(legoSet2) //
.map(it -> jdbc.queryForMap("SELECT count(*) FROM repo_legoset"));
transactional.as(StepVerifier::create).expectNext(Collections.singletonMap("count", 0L)).verifyComplete();
nonTransactional.as(StepVerifier::create).expectNext(Collections.singletonMap("count", 2L)).verifyComplete();
Map<String, Object> count = jdbc.queryForMap("SELECT count(*) FROM repo_legoset");
assertThat(count).containsEntry("count", 2L);
}
interface LegoSetRepository extends ReactiveCrudRepository<LegoSet, Integer> {
@Query("SELECT * FROM repo_legoset WHERE name like $1")