Skip to content

Commit 07f6ec3

Browse files
authored
Firestore declarative transactions tests and docs (spring-attic#2039)
* declarative transactions tests and docs; fixes spring-attic#2004
1 parent c8cbb54 commit 07f6ec3

File tree

6 files changed

+205
-10
lines changed

6 files changed

+205
-10
lines changed

docs/src/main/asciidoc/firestore.adoc

+38
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,44 @@ Read documents are locked until the transaction finishes with a commit or a roll
180180
If an `Exception` is thrown within a transaction, the rollback operation is executed.
181181
Otherwise, the commit operation is executed.
182182

183+
===== Declarative Transactions with @Transactional Annotation
184+
185+
This feature requires a bean of `SpannerTransactionManager`, which is provided when using `spring-cloud-gcp-starter-data-firestore`.
186+
187+
`FirestoreTemplate` and `FirestoreReactiveRepository` support running methods with the `@Transactional` https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative[annotation] as transactions.
188+
If a method annotated with `@Transactional` calls another method also annotated, then both methods will work within the same transaction.
189+
190+
One way to use this feature is illustrated here. You would need to do the following:
191+
192+
. Annotate your configuration class with the `@EnableTransactionManagement` annotation.
193+
194+
. Create a service class that has methods annotated with `@Transactional`:
195+
196+
[source, java, indent=0]
197+
----
198+
include::{project-root}/spring-cloud-gcp-data-firestore/src/test/java/org/springframework/cloud/gcp/data/firestore/it/UserService.java[tag=user_service]
199+
----
200+
201+
[start=3]
202+
. Make a Spring Bean provider that creates an instance of that class:
203+
204+
[source, java, indent=0]
205+
----
206+
include::{project-root}/spring-cloud-gcp-data-firestore/src/test/java/org/springframework/cloud/gcp/data/firestore/it/FirestoreIntegrationTestsConfiguration.java[tag=user_service_bean]
207+
----
208+
209+
After that, you can autowire your service like so:
210+
[source, java]
211+
----
212+
public class MyApplication {
213+
include::{project-root}/spring-cloud-gcp-data-firestore/src/test/java/org/springframework/cloud/gcp/data/firestore/it/FirestoreRepositoryIntegrationTests.java[tag=autowire_user_service]
214+
}
215+
----
216+
217+
Now when you call the methods annotated with `@Transactional` on your service object, a transaction will be automatically started.
218+
If an error occurs during the execution of a method annotated with `@Transactional`, the transaction will be rolled back.
219+
If no error occurs, the transaction will be committed.
220+
183221
=== Reactive Repository Sample
184222

185223
A https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-data-firestore-sample[sample application] is available.

spring-cloud-gcp-data-firestore/src/main/java/org/springframework/cloud/gcp/data/firestore/transaction/ReactiveFirestoreTransactionManager.java

+1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ protected Mono<Void> doRollback(TransactionSynchronizationManager transactionSyn
114114
obs -> this.firestore.rollback(RollbackRequest.newBuilder()
115115
.setTransaction(
116116
extractFirestoreTransaction(genericReactiveTransaction).getTransactionId())
117+
.setDatabase(this.databasePath)
117118
.build(), obs))
118119
.then();
119120
}

spring-cloud-gcp-data-firestore/src/test/java/org/springframework/cloud/gcp/data/firestore/it/FirestoreIntegrationTests.java

+27-8
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
import static org.assertj.core.api.Assertions.assertThat;
4242
import static org.hamcrest.Matchers.is;
4343
import static org.junit.Assume.assumeThat;
44+
import static org.mockito.ArgumentMatchers.any;
45+
import static org.mockito.Mockito.reset;
46+
import static org.mockito.Mockito.times;
47+
import static org.mockito.Mockito.verify;
4448

4549
/**
4650
* @author Dmitry Solomakha
@@ -79,23 +83,38 @@ public void transactionTest() {
7983
User alice = new User("Alice", 29);
8084
User bob = new User("Bob", 60);
8185

86+
8287
DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
8388
transactionDefinition.setReadOnly(false);
8489
TransactionalOperator operator = TransactionalOperator.create(this.txManager, transactionDefinition);
8590

86-
this.firestoreTemplate.save(alice)
87-
.then(this.firestoreTemplate.save(bob))
88-
.as(operator::transactional).block();
91+
reset(this.txManager);
92+
93+
this.firestoreTemplate.save(alice).then(this.firestoreTemplate.save(bob))
94+
.as(operator::transactional)
95+
.block();
8996

9097
assertThat(this.firestoreTemplate.findAll(User.class).collectList().block())
9198
.containsExactlyInAnyOrder(bob, alice);
9299

100+
verify(this.txManager, times(1)).commit(any());
101+
verify(this.txManager, times(0)).rollback(any());
102+
verify(this.txManager, times(1)).getReactiveTransaction(any());
103+
104+
reset(this.txManager);
93105

94106
// test rollback
95107
this.firestoreTemplate.saveAll(Mono.defer(() -> {
96108
throw new FirestoreDataException("BOOM!");
97109
}))
98-
.then(this.firestoreTemplate.deleteAll(User.class)).onErrorReturn(0L).block();
110+
.then(this.firestoreTemplate.deleteAll(User.class))
111+
.as(operator::transactional)
112+
.onErrorResume(throwable -> Mono.empty())
113+
.block();
114+
115+
verify(this.txManager, times(0)).commit(any());
116+
verify(this.txManager, times(1)).rollback(any());
117+
verify(this.txManager, times(1)).getReactiveTransaction(any());
99118

100119
assertThat(this.firestoreTemplate.count(User.class).block()).isEqualTo(2);
101120

@@ -148,7 +167,7 @@ public void writeReadDeleteTest() {
148167

149168

150169
@Test
151-
public void saveTest() throws InterruptedException {
170+
public void saveTest() {
152171
assertThat(this.firestoreTemplate.count(User.class).block()).isEqualTo(0);
153172

154173
User u1 = new User("Cloud", 22);
@@ -159,7 +178,7 @@ public void saveTest() throws InterruptedException {
159178
}
160179

161180
@Test
162-
public void saveAllTest() throws InterruptedException {
181+
public void saveAllTest() {
163182
User u1 = new User("Cloud", 22);
164183
User u2 = new User("Squall", 17);
165184
Flux<User> users = Flux.fromArray(new User[]{u1, u2});
@@ -173,7 +192,7 @@ public void saveAllTest() throws InterruptedException {
173192
}
174193

175194
@Test
176-
public void saveAllBulkTest() throws InterruptedException {
195+
public void saveAllBulkTest() {
177196
Flux<User> users = Flux.create(sink -> {
178197
for (int i = 0; i < 1000; i++) {
179198
sink.next(new User("testUser " + i, i));
@@ -189,7 +208,7 @@ public void saveAllBulkTest() throws InterruptedException {
189208
}
190209

191210
@Test
192-
public void deleteTest() throws InterruptedException {
211+
public void deleteTest() {
193212
this.firestoreTemplate.save(new User("alpha", 45)).block();
194213
this.firestoreTemplate.save(new User("beta", 23)).block();
195214
this.firestoreTemplate.save(new User("gamma", 44)).block();

spring-cloud-gcp-data-firestore/src/test/java/org/springframework/cloud/gcp/data/firestore/it/FirestoreIntegrationTestsConfiguration.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.grpc.ManagedChannel;
2525
import io.grpc.ManagedChannelBuilder;
2626
import io.grpc.auth.MoreCallCredentials;
27+
import org.mockito.Mockito;
2728

2829
import org.springframework.beans.factory.annotation.Value;
2930
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -34,13 +35,15 @@
3435
import org.springframework.context.annotation.Bean;
3536
import org.springframework.context.annotation.Configuration;
3637
import org.springframework.context.annotation.PropertySource;
38+
import org.springframework.transaction.annotation.EnableTransactionManagement;
3739

3840
/**
3941
* Spring config for the integration tests.
4042
*/
4143
@Configuration
4244
@PropertySource("application-test.properties")
4345
@EnableReactiveFirestoreRepositories
46+
@EnableTransactionManagement
4447
public class FirestoreIntegrationTestsConfiguration {
4548
@Value("projects/${test.integration.firestore.project-id}/databases/(default)/documents")
4649
String defaultParent;
@@ -72,7 +75,14 @@ public FirestoreMappingContext firestoreMappingContext() {
7275
@ConditionalOnMissingBean
7376
public ReactiveFirestoreTransactionManager firestoreTransactionManager(
7477
FirestoreGrpc.FirestoreStub firestoreStub) {
75-
return new ReactiveFirestoreTransactionManager(firestoreStub, this.defaultParent);
78+
return Mockito.spy(new ReactiveFirestoreTransactionManager(firestoreStub, this.defaultParent));
7679
}
7780

81+
//tag::user_service_bean[]
82+
@Bean
83+
public UserService userService() {
84+
return new UserService();
85+
}
86+
//end::user_service_bean[]
87+
7888
}

spring-cloud-gcp-data-firestore/src/test/java/org/springframework/cloud/gcp/data/firestore/it/FirestoreRepositoryIntegrationTests.java

+58-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.junit.Test;
2626
import org.junit.runner.RunWith;
2727
import reactor.core.publisher.Flux;
28+
import reactor.core.publisher.Mono;
2829

2930
import org.springframework.beans.factory.annotation.Autowired;
3031
import org.springframework.cloud.gcp.data.firestore.User;
@@ -38,6 +39,10 @@
3839
import static org.assertj.core.api.Assertions.assertThat;
3940
import static org.hamcrest.Matchers.is;
4041
import static org.junit.Assume.assumeThat;
42+
import static org.mockito.ArgumentMatchers.any;
43+
import static org.mockito.Mockito.reset;
44+
import static org.mockito.Mockito.times;
45+
import static org.mockito.Mockito.verify;
4146

4247
@RunWith(SpringRunner.class)
4348
@ContextConfiguration(classes = FirestoreIntegrationTestsConfiguration.class)
@@ -52,6 +57,14 @@ public class FirestoreRepositoryIntegrationTests {
5257
ReactiveFirestoreTransactionManager txManager;
5358
//end::autowire_tx_manager[]
5459

60+
//tag::autowire_user_service[]
61+
@Autowired
62+
UserService userService;
63+
//end::autowire_user_service[]
64+
65+
@Autowired
66+
ReactiveFirestoreTransactionManager transactionManager;
67+
5568
@BeforeClass
5669
public static void checkToRun() throws IOException {
5770
assumeThat("Firestore-sample tests are disabled. "
@@ -62,6 +75,7 @@ public static void checkToRun() throws IOException {
6275
@Before
6376
public void cleanTestEnvironment() {
6477
this.userRepository.deleteAll().block();
78+
reset(this.transactionManager);
6579
}
6680

6781
@Test
@@ -124,7 +138,7 @@ public void transactionalOperatorTest() {
124138
public void partTreeRepositoryMethodTest() {
125139
User u1 = new User("Cloud", 22);
126140
User u2 = new User("Squall", 17);
127-
Flux<User> users = Flux.fromArray(new User[] { u1, u2 });
141+
Flux<User> users = Flux.fromArray(new User[] {u1, u2});
128142

129143
this.userRepository.saveAll(users).blockLast();
130144

@@ -151,4 +165,47 @@ public void pageableQueryTest() {
151165

152166
assertThat(pagedUsers).containsExactlyInAnyOrder("blah-person5", "blah-person6");
153167
}
168+
169+
@Test
170+
public void declarativeTransactionRollbackTest() {
171+
this.userService.deleteUsers().onErrorResume(throwable -> Mono.empty()).block();
172+
173+
verify(this.transactionManager, times(0)).commit(any());
174+
verify(this.transactionManager, times(1)).rollback(any());
175+
verify(this.transactionManager, times(1)).getReactiveTransaction(any());
176+
}
177+
178+
@Test
179+
public void declarativeTransactionCommitTest() {
180+
User alice = new User("Alice", 29);
181+
User bob = new User("Bob", 60);
182+
183+
this.userRepository.save(alice).then(this.userRepository.save(bob)).block();
184+
185+
this.userService.updateUsers().block();
186+
187+
verify(this.transactionManager, times(1)).commit(any());
188+
verify(this.transactionManager, times(0)).rollback(any());
189+
verify(this.transactionManager, times(1)).getReactiveTransaction(any());
190+
191+
assertThat(this.userRepository.findAll().map(User::getAge).collectList().block())
192+
.containsExactlyInAnyOrder(28, 59);
193+
}
194+
195+
@Test
196+
public void transactionPropagationTest() {
197+
User alice = new User("Alice", 29);
198+
User bob = new User("Bob", 60);
199+
200+
this.userRepository.save(alice).then(this.userRepository.save(bob)).block();
201+
202+
this.userService.updateUsersTransactionPropagation().block();
203+
204+
verify(this.transactionManager, times(1)).commit(any());
205+
verify(this.transactionManager, times(0)).rollback(any());
206+
verify(this.transactionManager, times(1)).getReactiveTransaction(any());
207+
208+
assertThat(this.userRepository.findAll().map(User::getAge).collectList().block())
209+
.containsExactlyInAnyOrder(28, 59);
210+
}
154211
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2017-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gcp.data.firestore.it;
18+
19+
import reactor.core.publisher.Flux;
20+
import reactor.core.publisher.Mono;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.cloud.gcp.data.firestore.FirestoreDataException;
24+
import org.springframework.cloud.gcp.data.firestore.User;
25+
import org.springframework.transaction.annotation.Transactional;
26+
27+
/**
28+
* @author Dmitry Solomakha
29+
*/
30+
31+
//tag::user_service[]
32+
class UserService {
33+
@Autowired
34+
private UserRepository userRepository;
35+
36+
//end::user_service[]
37+
@Transactional
38+
public Mono<Void> updateUsersTransactionPropagation() {
39+
return findAll()
40+
.flatMap(a -> {
41+
a.setAge(a.getAge() - 1);
42+
return this.userRepository.save(a);
43+
})
44+
.then();
45+
}
46+
47+
@Transactional
48+
private Flux<User> findAll() {
49+
return this.userRepository.findAll();
50+
}
51+
52+
@Transactional
53+
public Mono<Void> deleteUsers() {
54+
return this.userRepository.saveAll(Mono.defer(() -> {
55+
throw new FirestoreDataException("BOOM!");
56+
})).then(this.userRepository.deleteAll());
57+
}
58+
59+
// tag::user_service[]
60+
@Transactional
61+
public Mono<Void> updateUsers() {
62+
return this.userRepository.findAll()
63+
.flatMap(a -> {
64+
a.setAge(a.getAge() - 1);
65+
return this.userRepository.save(a);
66+
})
67+
.then();
68+
}
69+
}
70+
//end::user_service[]

0 commit comments

Comments
 (0)