#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:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user