|
1 | 1 | [[couchbase.transactions]]
|
2 |
| -= Transaction Support |
| 2 | += Couchbase Transactions |
3 | 3 |
|
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. |
5 | 5 |
|
6 | 6 | == Requirements
|
7 | 7 |
|
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. |
10 | 10 | - 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. |
11 | 12 |
|
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. |
15 | 15 |
|
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 |
22 | 17 |
|
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 |
24 | 36 | ====
|
| 37 | +.The Configuration |
25 | 38 | [source,java]
|
26 | 39 | ----
|
27 | 40 | @Configuration
|
| 41 | +@EnableCouchbaseRepositories("<parent-dir-of-repository-interfaces>") |
| 42 | +@EnableReactiveCouchbaseRepositories("<parent-dir-of-repository-interfaces>") |
| 43 | +@EnableTransactionManagement // <1> |
28 | 44 | static class Config extends AbstractCouchbaseConfiguration {
|
29 | 45 |
|
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; |
35 | 78 |
|
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 |
| - } |
| 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 | + } |
| 94 | +
|
| 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 | + } |
42 | 100 |
|
43 | 101 | }
|
44 | 102 | ----
|
| 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. |
45 | 121 | ====
|
46 | 122 |
|
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 |
| 124 | + |
| 125 | +CouchbaseTransactionalOperator can be used to construct a tansaction 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 the following (and not TransactionalOperator.create(manager) as described in general Spring Data transaction documenation). |
51 | 128 |
|
52 |
| -.Transaction Access |
| 129 | +.TransactionalOperator Construction |
53 | 130 | ====
|
54 | 131 | [source,java]
|
55 | 132 | ----
|
56 |
| -@Autowired |
57 |
| -Transactions transactions; |
| 133 | +public static CouchbaseTransactionalOperator create(CouchbaseCallbackTransactionManager manager) |
| 134 | +==== |
58 | 135 |
|
59 |
| -@Autowired |
60 |
| -CouchbaseClientFactory couchbaseClientFactory; |
| 136 | +.Transaction Access Using TransactionalOperator.execute() |
| 137 | +==== |
| 138 | +[source,java] |
| 139 | +---- |
| 140 | +@Autowired TransactionalOperator txOperator; |
| 141 | +@Autowired ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; |
61 | 142 |
|
62 |
| -public void doSomething() { |
63 |
| - transactions.run(ctx -> { |
64 |
| - ctx.insert(couchbaseClientFactory.getDefaultCollection(), "id", "content"); |
65 |
| - ctx.commit(); |
66 |
| - }); |
67 |
| -} |
| 143 | +Person person = reactiveCouchbaseTemplate.insertById(Person.class).inCollection(cName).one(WalterWhite).block(); |
| 144 | +Flux<Person> result = txOperator.execute((ctx) -> reactiveCouchbaseTemplate.findById(Person.class).one(person.id()) |
| 145 | + .flatMap(p -> reactiveCouchbaseTemplate.replaceById(Person.class).one(p.withFirstName("Walt")))); |
| 146 | +result.blockLast(); |
68 | 147 | ----
|
69 | 148 | ====
|
70 | 149 |
|
71 |
| -== Object Conversions |
| 150 | +== Transactions Directly with the SDK |
72 | 151 |
|
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: |
| 152 | +Please see the https://docs.couchbase.com/java-sdk/current/howtos/distributed-acid-transactions-from-the-sdk.html[Reference Documentation] |
76 | 153 |
|
77 |
| -.Transaction Conversion on Write |
| 154 | +.Transaction Access - Blocking |
78 | 155 | ====
|
79 | 156 | [source,java]
|
80 | 157 | ----
|
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()); |
| 158 | +@Autowired ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; |
92 | 159 |
|
93 |
| - ctx.commit(); |
94 |
| - }); |
95 |
| -} |
| 160 | +TransactionResult result = reactiveCouchbaseTemplate.getCouchbaseClientFactory().getCluster().transactions().run(ctx -> { |
| 161 | + return reactiveCouchbaseTemplate.insertById(Person.class).one(WalterWhite); |
| 162 | +}); |
96 | 163 | ----
|
97 | 164 | ====
|
98 | 165 |
|
99 |
| -The same approach can be used on read: |
100 |
| - |
101 |
| -.Transaction Conversion on Read |
| 166 | +.Transaction Access - Reactive |
102 | 167 | ====
|
103 | 168 | [source,java]
|
104 | 169 | ----
|
105 |
| -TransactionGetResult getResult = ctx.get(couchbaseClientFactory.getDefaultCollection(), "doc-id"); |
| 170 | +@Autowired ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; |
106 | 171 |
|
107 |
| -CouchbaseDocument source = new CouchbaseDocument(getResult.id()); |
108 |
| -source.setContent(getResult.contentAsObject()); |
109 |
| -Airline read = mappingCouchbaseConverter.read(Airline.class, source); |
| 172 | +TransactionResult result = reactiveCouchbaseTemplate.getCouchbaseClientFactory().getCluster().reactive().transactions() |
| 173 | + .run(ctx -> TransactionalSupport.checkForTransactionInThreadLocalStorage().then(Mono.defer(() -> { |
| 174 | + reactiveCouchbaseTemplate.insertById(Person.class).one(WalterWhite); |
| 175 | + }))).block(); |
110 | 176 | ----
|
111 | 177 | ====
|
112 | 178 |
|
113 |
| -We are also looking into tighter integration of the transaction library into the spring data library |
114 |
| -ecosystem. |
| 179 | + |
0 commit comments