Skip to content

Commit 28d9f8f

Browse files
committed
Update transactions documentation for native transaction support.
Closes #1454,#1512.
1 parent 258dc55 commit 28d9f8f

19 files changed

+292
-210
lines changed

README.adoc

+23
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,29 @@ Building the documentation builds also the project without running tests.
179179

180180
The generated documentation is available from `target/site/reference/html/index.html`.
181181

182+
=== Building and staging reference documentation for review
183+
184+
[source,bash]
185+
----
186+
export MY_GIT_USER=<github-user>
187+
mvn generate-resources
188+
docs=`pwd`/target/site/reference/html
189+
pushd /tmp
190+
mkdir $$
191+
cd $$
192+
# see https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site
193+
# this examples uses a repository named "staged"
194+
git clone [email protected]:${MY_GIT_USER}/staged.git -b gh-pages
195+
cd staged
196+
cp -R $docs/* .
197+
git add .
198+
git commit --message "stage for review"
199+
git push origin gh-pages
200+
popd
201+
----
202+
203+
The generated documentation is available from `target/site/reference/html/index.html`.
204+
182205
== Examples
183206

184207
* https://github.com/spring-projects/spring-data-examples/[Spring Data Examples] contains example projects that explain specific features in more detail.

pom.xml

+12
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,20 @@
301301
<artifactId>maven-assembly-plugin</artifactId>
302302
</plugin>
303303
<plugin>
304+
<!-- generate asciidoc to stage for review -->
304305
<groupId>org.asciidoctor</groupId>
305306
<artifactId>asciidoctor-maven-plugin</artifactId>
307+
<executions>
308+
<execution>
309+
<phase>generate-resources</phase>
310+
<goals>
311+
<goal>process-asciidoc</goal>
312+
</goals>
313+
<configuration>
314+
<outputDirectory>target/site/reference/html</outputDirectory>
315+
</configuration>
316+
</execution>
317+
</executions>
306318
</plugin>
307319
<plugin>
308320
<groupId>com.mysema.maven</groupId>

src/main/asciidoc/transactions.adoc

+133-69
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,178 @@
11
[[couchbase.transactions]]
2-
= Transaction Support
2+
= Couchbase Transactions
33

4-
Couchbase supports https://docs.couchbase.com/server/6.5/learn/data/transactions.html[Distributed Transactions]. This section documents on how to use it with Spring Data Couchbase.
4+
Couchbase supports https://docs.couchbase.com/server/current/learn/data/transactions.html[Distributed Transactions]. This section documents how to use it with Spring Data Couchbase.
55

66
== Requirements
77

8-
- Couchbase Server 6.5 or above.
9-
- Couchbase Java client 3.0.0 or above. It is recommended to follow the transitive dependency for the transactions library from maven.
8+
- Couchbase Server 6.6.1 or aabove.
9+
- Spring Data Couchbase 5.0.0-M5 or above.
1010
- NTP should be configured so nodes of the Couchbase cluster are in sync with time. The time being out of sync will not cause incorrect behavior, but can impact metadata cleanup.
11+
- Set spring.main.allow-bean-definition-overriding=true either in application.properties or as a SpringApplicationBuilder property.
1112

12-
== Getting Started & Configuration
13-
14-
The `couchbase-transactions` artifact needs to be included into your `pom.xml` if maven is being used (or equivalent).
13+
== Overview
14+
The Spring Data Couchbase template operations insert, find, replace and delete and repository methods that use those calls can participate in a Couchbase Transaction. They can be executed in a transaction by using the @Transactional annotation, the CouchbaseTransactionalOperator, or in the lambda of a Couchbase Transaction.
1515

16-
- Group: `com.couchbase.client`
17-
- Artifact: `couchbase-transactions`
18-
- Version: latest one, i.e. `1.0.0`
19-
20-
Once it is included in your project, you need to create a single `Transactions` object. Conveniently, it can be part of
21-
your spring data couchbase `AbstractCouchbaseConfiguration` implementation:
16+
== Getting Started & Configuration
2217

23-
.Transaction Configuration
18+
Couchbase Transactions are normally leveraged with a method annotated with @Transactional.
19+
The @Transactional operator is implemented with the CouchbaseTransactionManager which is supplied as a bean in the AbstractCouchbaseConfiguration.
20+
Couchbase Transactions can be used without defining a service class by using CouchbaseTransactionOperator which is also supplied as a bean in AbtractCouchbaseConfiguration.
21+
Couchbase Transactions can also be used directly using Spring Data Couchbase operations within a lambda https://docs.couchbase.com/server/current/learn/data/transactions.html#using-transactions[Using Transactions]
22+
23+
== Transactions with @Transactional
24+
25+
@Transactional defines as transactional a method or all methods on a class.
26+
27+
When this annotation is declared at the class level, it applies as a default
28+
to all methods of the declaring class and its subclasses.
29+
30+
=== Attribute Semantics
31+
32+
In this release, the Couchbase Transactions ignores the rollback attributes.
33+
The transaction isolation level is read-committed;
34+
35+
.Transaction Configuration and Use by @Transactional
2436
====
37+
.The Configuration
2538
[source,java]
2639
----
2740
@Configuration
41+
@EnableCouchbaseRepositories("<parent-dir-of-repository-interfaces>")
42+
@EnableReactiveCouchbaseRepositories("<parent-dir-of-repository-interfaces>")
43+
@EnableTransactionManagement // <1>
2844
static class Config extends AbstractCouchbaseConfiguration {
2945
30-
// Usual Setup
31-
@Override public String getConnectionString() { /* ... */ }
32-
@Override public String getUserName() { /* ... */ }
33-
@Override public String getPassword() { /* ... */ }
34-
@Override public String getBucketName() { /* ... */ }
46+
// Usual Setup
47+
@Override public String getConnectionString() { /* ... */ }
48+
@Override public String getUserName() { /* ... */ }
49+
@Override public String getPassword() { /* ... */ }
50+
@Override public String getBucketName() { /* ... */ }
51+
52+
// Customization of transaction behavior is via the configureEnvironment() method
53+
@Override protected void configureEnvironment(final Builder builder) {
54+
builder.transactionsConfig(
55+
TransactionsConfig.builder().timeout(Duration.ofSeconds(30)));
56+
}
57+
}
58+
----
59+
.The Transactional Service Class
60+
Note that the body of @Transactional methods can be re-executed if the transaction fails.
61+
It is imperative that everthing in the method body be idempotent.
62+
[source,java]
63+
----
64+
import reactor.core.publisher.Mono;
65+
import reactor.core.publisher.Flux;
66+
67+
import org.springframework.stereotype.Service;
68+
import org.springframework.transaction.annotation.Transactional;
69+
70+
final CouchbaseOperations personOperations;
71+
final ReactiveCouchbaseOperations reactivePersonOperations;
72+
73+
@Service // <2>
74+
public class PersonService {
75+
76+
final CouchbaseOperations operations;
77+
final ReactiveCouchbaseOperations reactiveOperations;
78+
79+
public PersonService(CouchbaseOperations ops, ReactiveCouchbaseOperations reactiveOps) {
80+
operations = ops;
81+
reactiveOperations = reactiveOps;
82+
}
83+
84+
// no annotation results in this method being executed not in a transaction
85+
public Person save(Person p) {
86+
return operations.save(p);
87+
}
88+
89+
@Transactional
90+
public Person changeFirstName(String id, String newFirstName) {
91+
Person p = operations.findById(Person.class).one(id); // <3>
92+
return operations.replaceById(Person.class).one(p.withFirstName(newFirstName);
93+
}
3594
36-
@Bean
37-
public Transactions transactions(final Cluster couchbaseCluster) {
38-
return Transactions.create(couchbaseCluster, TransactionConfigBuilder.create()
39-
// The configuration can be altered here, but in most cases the defaults are fine.
40-
.build());
41-
}
95+
@Transactional
96+
public Mono<Person> reactiveChangeFirstName(String id, String newFirstName) {
97+
return personOperationsRx.findById(Person.class).one(person.id())
98+
.flatMap(p -> personOperationsRx.replaceById(Person.class).one(p.withFirstName(newFirstName)));
99+
}
42100
43101
}
44102
----
103+
[source,java]
104+
.Using the @Transactional Service.
105+
----
106+
@Autowired PersonService personService; // <4>
107+
108+
Person walterWhite = new Person( "Walter", "White");
109+
Person p = personService.save(walterWhite); // this is not a transactional method
110+
...
111+
Person renamedPerson = personService.changeFirstName(walterWhite.getId(), "Ricky"); // <5>
112+
----
113+
Functioning of the @Transactional method annotation requires
114+
[start=1]
115+
. the configuration class to be annotated with @EnableTransactionManagement;
116+
. the service object with the annotated methods must be annotated with @Service;
117+
. the body of the method is executed in a transaction.
118+
. the service object with the annotated methods must be obtained via @Autowired.
119+
. the call to the method must be made from a different class than service because calling an annotated
120+
method from the same class will not invoke the Method Interceptor that does the transaction processing.
45121
====
46122

47-
Once the `@Bean` is configured, you can autowire it from your service (or any other class) to make use of it. Please
48-
see the https://docs.couchbase.com/java-sdk/3.0/howtos/distributed-acid-transactions-from-the-sdk.html[Reference Documentation]
49-
on how to use the `Transactions` class. Since you need access to the current `Collection` as well, we recommend you to also
50-
autowire the `CouchbaseClientFactory` and access it from there:
123+
== Transactions with CouchbaseTransactionalOperator
51124

52-
.Transaction Access
125+
CouchbaseTransactionalOperator can be used to construct a transaction in-line without creating a service class that uses @Transactional.
126+
CouchbaseTransactionalOperator is available as a bean and can be instantiated with @Autowired.
127+
If creating one explicitly, it must be created with CouchbaseTransactionalOperator.create(manager) (NOT TransactionalOperator.create(manager)).
128+
129+
.Transaction Access Using TransactionalOperator.execute()
53130
====
54131
[source,java]
55132
----
56-
@Autowired
57-
Transactions transactions;
58-
59-
@Autowired
60-
CouchbaseClientFactory couchbaseClientFactory;
133+
@Autowired TransactionalOperator txOperator;
134+
@Autowired ReactiveCouchbaseTemplate reactiveCouchbaseTemplate;
61135
62-
public void doSomething() {
63-
transactions.run(ctx -> {
64-
ctx.insert(couchbaseClientFactory.getDefaultCollection(), "id", "content");
65-
ctx.commit();
66-
});
67-
}
136+
Flux<Person> result = txOperator.execute((ctx) ->
137+
reactiveCouchbaseTemplate.findById(Person.class).one(person.id())
138+
.flatMap(p -> reactiveCouchbaseTemplate.replaceById(Person.class).one(p.withFirstName("Walt")))
139+
);
68140
----
69141
====
70142

71-
== Object Conversions
143+
== Transactions Directly with the SDK
72144

73-
Since the transactions library itself has no knowledge of your spring data entity types, you need to convert it back and
74-
forth when reading/writing to interact properly. Fortunately, all you need to do is autowire the `MappingCouchbaseConverter` and
75-
utilize it:
145+
Spring Data Couchbase works seamlessly with the Couchbase Java SDK for transaction processing. Spring Data Couchbase operations that
146+
can be executed in a transaction will work directly within the lambda of a transactions().run() without involving any of the Spring
147+
Transactions mechanisms. This is the most straight-forward way to leverage Couchbase Transactions in Spring Data Couchbase.
76148

77-
.Transaction Conversion on Write
149+
Please see the https://docs.couchbase.com/java-sdk/current/howtos/distributed-acid-transactions-from-the-sdk.html[Reference Documentation]
150+
151+
.Transaction Access - Blocking
78152
====
79153
[source,java]
80154
----
81-
@Autowired
82-
MappingCouchbaseConverter mappingCouchbaseConverter;
83-
84-
public void doSomething() {
85-
transactions.run(ctx -> {
86-
87-
Airline airline = new Airline("demo-airline", "at");
88-
CouchbaseDocument target = new CouchbaseDocument();
89-
mappingCouchbaseConverter.write(airline, target);
90-
91-
ctx.insert(couchbaseClientFactory.getDefaultCollection(), target.getId(), target.getContent());
155+
@Autowired CouchbaseTemplate couchbaseTemplate;
92156
93-
ctx.commit();
94-
});
95-
}
157+
TransactionResult result = couchbaseTemplate.getCouchbaseClientFactory().getCluster().transactions().run(ctx -> {
158+
Person p = couchbaseTemplate.findById(Person.class).one(personId);
159+
couchbaseTemplate.replaceById(Person.class).one(p.withFirstName("Walt"));
160+
});
96161
----
97162
====
98163

99-
The same approach can be used on read:
100-
101-
.Transaction Conversion on Read
164+
.Transaction Access - Reactive
102165
====
103166
[source,java]
104167
----
105-
TransactionGetResult getResult = ctx.get(couchbaseClientFactory.getDefaultCollection(), "doc-id");
168+
@Autowired ReactiveCouchbaseTemplate reactiveCouchbaseTemplate;
106169
107-
CouchbaseDocument source = new CouchbaseDocument(getResult.id());
108-
source.setContent(getResult.contentAsObject());
109-
Airline read = mappingCouchbaseConverter.read(Airline.class, source);
170+
Mono<TransactionResult> result = reactiveCouchbaseTemplate.getCouchbaseClientFactory().getCluster().reactive().transactions()
171+
.run(ctx ->
172+
reactiveCouchbaseTemplate.findById(Person.class).one(personId)
173+
.flatMap(p -> reactiveCouchbaseTemplate.replaceById(Person.class).one(p.withFirstName("Walt")))
174+
);
110175
----
111176
====
112177

113-
We are also looking into tighter integration of the transaction library into the spring data library
114-
ecosystem.
178+

src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java

+11
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
5252
import org.springframework.transaction.interceptor.TransactionAttributeSource;
5353
import org.springframework.transaction.interceptor.TransactionInterceptor;
54+
import org.springframework.transaction.support.TransactionTemplate;
5455
import org.springframework.util.ClassUtils;
5556
import org.springframework.util.StringUtils;
5657

@@ -330,6 +331,16 @@ CouchbaseCallbackTransactionManager couchbaseTransactionManager(CouchbaseClientF
330331
return new CouchbaseCallbackTransactionManager(clientFactory);
331332
}
332333

334+
/**
335+
* The default transaction template manager.
336+
*
337+
* @param couchbaseTransactionManager
338+
* @return
339+
*/
340+
@Bean(BeanNames.COUCHBASE_TRANSACTION_TEMPLATE)
341+
TransactionTemplate couchbaseTransactionTemplate(CouchbaseCallbackTransactionManager couchbaseTransactionManager) {
342+
return new TransactionTemplate(couchbaseTransactionManager);
343+
}
333344
/**
334345
* The default TransactionalOperator.
335346
*

src/main/java/org/springframework/data/couchbase/config/BeanNames.java

+2
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,7 @@ public class BeanNames {
6464

6565
public static final String COUCHBASE_TRANSACTION_MANAGER = "couchbaseTransactionManager";
6666

67+
public static final String COUCHBASE_TRANSACTION_TEMPLATE = "couchbaseTransactionTemplate";
68+
6769
public static final String COUCHBASE_TRANSACTIONAL_OPERATOR = "couchbaseTransactionalOperator";
6870
}

src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public void shouldRollbackAfterException() {
108108

109109
@Test
110110
public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() {
111-
assertThrowsWithCause(() -> personService.declarativeSavePersonErrors(WalterWhite).blockLast(),
111+
assertThrowsWithCause(() -> personService.declarativeSavePersonErrors(WalterWhite).block(),
112112
TransactionSystemUnambiguousException.class, SimulateFailureException.class);
113113
}
114114

src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import org.springframework.data.couchbase.util.ClusterType;
3737
import org.springframework.data.couchbase.util.IgnoreWhen;
3838
import org.springframework.data.couchbase.util.JavaIntegrationTests;
39-
import org.springframework.stereotype.Component;
4039
import org.springframework.stereotype.Service;
4140
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
4241
import org.springframework.transaction.annotation.EnableTransactionManagement;
@@ -114,9 +113,7 @@ public void upsertById() {
114113
});
115114
}
116115

117-
@Service
118-
@Component
119-
@EnableTransactionManagement
116+
@Service // this will work in the unit tests even without @Service because of explicit loading by @SpringJUnitConfig
120117
static class PersonService {
121118
final CouchbaseOperations personOperations;
122119

src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import org.springframework.data.couchbase.util.ClusterType;
3737
import org.springframework.data.couchbase.util.IgnoreWhen;
3838
import org.springframework.data.couchbase.util.JavaIntegrationTests;
39-
import org.springframework.stereotype.Component;
4039
import org.springframework.stereotype.Service;
4140
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
4241
import org.springframework.transaction.annotation.EnableTransactionManagement;
@@ -100,9 +99,7 @@ public void supportedIsolation() {
10099
personService.supportedIsolation();
101100
}
102101

103-
@Service
104-
@Component
105-
@EnableTransactionManagement
102+
@Service // this will work in the unit tests even without @Service because of explicit loading by @SpringJUnitConfig
106103
static class PersonService {
107104
final CouchbaseOperations ops;
108105

src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
import org.springframework.data.couchbase.util.IgnoreWhen;
4444
import org.springframework.data.couchbase.util.JavaIntegrationTests;
4545
import org.springframework.lang.Nullable;
46-
import org.springframework.stereotype.Component;
4746
import org.springframework.stereotype.Service;
4847
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
4948
import org.springframework.transaction.IllegalTransactionStateException;
@@ -285,9 +284,7 @@ public void callDefaultThatCallsDefaultRetries() {
285284
assertEquals(3, attempts.get());
286285
}
287286

288-
@Service
289-
@Component
290-
@EnableTransactionManagement
287+
@Service // this will work in the unit tests even without @Service because of explicit loading by @SpringJUnitConfig
291288
static class PersonService {
292289
final CouchbaseOperations ops;
293290

0 commit comments

Comments
 (0)