|
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 | 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. |
| 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 | 11 |
|
12 | 12 | == Getting Started & Configuration
|
13 | 13 |
|
14 |
| -The `couchbase-transactions` artifact needs to be included into your `pom.xml` if maven is being used (or equivalent). |
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: |
| 14 | +Couchbase Transactions are normally leveraged with the @Transactional operator. |
| 15 | +The @Transactional operator is implemented with the CouchbaseTransactionManager which is supplied as a bean in the AbstractCouchbaseConfiguration. |
22 | 16 |
|
23 | 17 | .Transaction Configuration
|
24 | 18 | ====
|
25 | 19 | [source,java]
|
26 | 20 | ----
|
27 | 21 | @Configuration
|
| 22 | +@EnableCouchbaseRepositories("<parent-dir-of-repository-interfaces>") |
| 23 | +@EnableReactiveCouchbaseRepositories("<parent-dir-of-repository-interfaces>") |
| 24 | +@EnableTransactionManagement // Add for @Transactional annotation support |
28 | 25 | static class Config extends AbstractCouchbaseConfiguration {
|
29 | 26 |
|
30 |
| - // Usual Setup |
31 |
| - @Override public String getConnectionString() { /* ... */ } |
32 |
| - @Override public String getUserName() { /* ... */ } |
33 |
| - @Override public String getPassword() { /* ... */ } |
34 |
| - @Override public String getBucketName() { /* ... */ } |
35 |
| -
|
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 |
| - } |
42 |
| -
|
| 27 | +// Usual Setup |
| 28 | +@Override public String getConnectionString() { /* ... */ } |
| 29 | +@Override public String getUserName() { /* ... */ } |
| 30 | +@Override public String getPassword() { /* ... */ } |
| 31 | +@Override public String getBucketName() { /* ... */ } |
| 32 | +
|
| 33 | +// Customization of transaction behavior is via the configureEnvironment() method |
| 34 | +@Override |
| 35 | +public void configureEnvironment(ClusterEnvironment.Builder builder) { |
| 36 | + // allow really long transactions |
| 37 | + builder.transactionsConfig( |
| 38 | + TransactionsConfig.builder().timeout(Duration.ofSeconds(30))); |
| 39 | + } |
43 | 40 | }
|
44 | 41 | ----
|
45 | 42 | ====
|
46 | 43 |
|
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: |
| 44 | + |
| 45 | +Functioning of the @Transactional method annotation requires |
| 46 | +[start=1] |
| 47 | +. the configuration class to be annotated with @EnableTransactionManagement; |
| 48 | +. the service object with the annotated methods must be annotated with @Service and @Component; |
| 49 | +. the service object with the annotated methods must be obtained as an @Bean either via @Autowire or |
| 50 | +from the application context; |
| 51 | +. the call to the method must be made from a different class than service because calling an annotated |
| 52 | +method from the same class will not invoke the Method Interceptor that does the transaction processing. |
| 53 | + |
| 54 | +== Transactions with @Transactional |
| 55 | + |
| 56 | +@Transactional Defines as transactional a method or all methods on a class. |
| 57 | + |
| 58 | +When this annotation is declared at the class level, it applies as a default |
| 59 | +to all methods of the declaring class and its subclasses. Note that it does not |
| 60 | +apply to ancestor classes up the class hierarchy; inherited methods need to be |
| 61 | +locally redeclared in order to participate in a subclass-level annotation. For |
| 62 | +details on method visibility constraints, consult the |
| 63 | +https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction[Transaction Management] |
| 64 | +section of the reference manual. |
| 65 | + |
| 66 | +=== Attribute Semantics |
| 67 | + |
| 68 | +In this release, he Couchbase Transactions implementation in |
| 69 | +Spring Data Couchbase ignores all @Transactional attributes. |
| 70 | +The transaction will roll back on uncaught exceptions. |
| 71 | +The transaction isoloation level is read committed; |
| 72 | + |
| 73 | +=== Transaction Management |
| 74 | + |
| 75 | +Blocking transactions are thread-bound and therefore do not propagate to newly started threads within the method. |
| 76 | +Non-blocking transactions are bound to the reactive context. |
51 | 77 |
|
52 | 78 | .Transaction Access
|
53 | 79 | ====
|
54 | 80 | [source,java]
|
55 | 81 | ----
|
56 |
| -@Autowired |
57 |
| -Transactions transactions; |
| 82 | +// this object must be provided by the Spring framework either by @Autowire or from ac.getBean(). |
| 83 | +// If created directly with its constructor, the method interceptor will not be called. |
| 84 | +@Autowired PersonService personService; |
| 85 | +
|
| 86 | +Person walterWhite = new Person( "Walter", "White"); |
| 87 | +Person p = personService.save(walterWhite); |
| 88 | +
|
| 89 | +---------- |
| 90 | +
|
| 91 | +import reactor.core.publisher.Mono; |
| 92 | +import reactor.core.publisher.Flux; |
| 93 | +
|
| 94 | +import org.springframework.stereotype.Component; |
| 95 | +import org.springframework.stereotype.Service; |
| 96 | +import org.springframework.transaction.annotation.Transactional; |
| 97 | +
|
| 98 | +final CouchbaseOperations personOperations; |
| 99 | +final ReactiveCouchbaseOperations reactivePersonOperations; |
| 100 | +
|
| 101 | +@Service |
| 102 | +@Component |
| 103 | +public class PersonService { |
| 104 | +
|
| 105 | + final CouchbaseOperations operations; |
| 106 | + final ReactiveCouchbaseOperations reactiveOperations; |
| 107 | +
|
| 108 | + // This method will be called for @Autowired using the |
| 109 | + // CouchbaseOperations and ReactiveCouchbaseOperations @Beans |
| 110 | + public PersonService(CouchbaseOperations ops, ReactiveCouchbaseOperations reactiveOps) { |
| 111 | + operations = ops; |
| 112 | + reactiveOperations = reactiveOps; |
| 113 | + } |
| 114 | +
|
| 115 | + @Transactional |
| 116 | + public Person save(Person p) { |
| 117 | + return operations.save(p); |
| 118 | + } |
| 119 | +
|
| 120 | + @Transactional |
| 121 | + public Person changeFirstName(String id, String newFirstName) { |
| 122 | + Person p = operations.findById(Person.class).one(id); |
| 123 | + return operations.replaceById(Person.class).one(p.withFirstName(newFirstName); |
| 124 | + } |
| 125 | +
|
| 126 | + @Transactional |
| 127 | + public Mono<Person> reactiveChangeFirstName(String id, String newFirstName) { |
| 128 | + return personOperationsRx.findById(Person.class).one(person.id()) |
| 129 | + .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p.withFirstName(newFirstName))); |
| 130 | + } |
58 | 131 |
|
59 |
| -@Autowired |
60 |
| -CouchbaseClientFactory couchbaseClientFactory; |
61 | 132 |
|
62 |
| -public void doSomething() { |
63 |
| - transactions.run(ctx -> { |
64 |
| - ctx.insert(couchbaseClientFactory.getDefaultCollection(), "id", "content"); |
65 |
| - ctx.commit(); |
66 |
| - }); |
67 | 133 | }
|
68 | 134 | ----
|
69 | 135 | ====
|
70 | 136 |
|
71 |
| -== Object Conversions |
| 137 | +== Transactions with CouchbaseTransactionalOperator |
72 | 138 |
|
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: |
| 139 | +CouchbaseTransactionalOperator can be used to construct a tansaction in-line without creating a service class that uses @Transactional. |
| 140 | +CouchbaseTransactionalOperator is available as a bean and can be instantiated with @Autowire. |
| 141 | +If creating one explicitly, it must be created with the following (and not TransactionalOperator.create(manager) as described in general Spring Data transaction documenation). |
| 142 | +.TransactionalOperator Construction |
| 143 | +==== |
| 144 | +[source,java] |
| 145 | +---- |
| 146 | +public static CouchbaseTransactionalOperator create(CouchbaseCallbackTransactionManager manager) |
| 147 | +==== |
76 | 148 |
|
77 |
| -.Transaction Conversion on Write |
| 149 | +.Transaction Access Using TransactionalOperator.execute() |
78 | 150 | ====
|
79 | 151 | [source,java]
|
80 | 152 | ----
|
81 |
| -@Autowired |
82 |
| -MappingCouchbaseConverter mappingCouchbaseConverter; |
| 153 | +@Autowired TransactionalOperator txOperator; |
83 | 154 |
|
84 |
| -public void doSomething() { |
85 |
| - transactions.run(ctx -> { |
| 155 | +Person person = reactiveCouchbaseTemplate.insertById(Person.class).inCollection(cName).one(WalterWhite).block(); |
| 156 | +Flux<Person> result = txOperator.execute((ctx) -> reactiveCouchbaseTemplate.findById(Person.class).one(person.id()) |
| 157 | + .flatMap(p -> reactiveCouchbaseTemplate.replaceById(Person.class).one(p.withFirstName("Walt")))); |
| 158 | +result.blockLast(); |
| 159 | +---- |
| 160 | +==== |
86 | 161 |
|
87 |
| - Airline airline = new Airline("demo-airline", "at"); |
88 |
| - CouchbaseDocument target = new CouchbaseDocument(); |
89 |
| - mappingCouchbaseConverter.write(airline, target); |
| 162 | +== Transactions with Only the SDK |
90 | 163 |
|
91 |
| - ctx.insert(couchbaseClientFactory.getDefaultCollection(), target.getId(), target.getContent()); |
| 164 | +Please see the https://docs.couchbase.com/java-sdk/current/howtos/distributed-acid-transactions-from-the-sdk.html[Reference Documentation] |
92 | 165 |
|
93 |
| - ctx.commit(); |
94 |
| - }); |
95 |
| -} |
96 |
| ----- |
| 166 | +.Transaction Access - Blocking |
97 | 167 | ====
|
| 168 | +[source,java] |
| 169 | +---- |
| 170 | +@Autowired CouchbaseTemplate template; |
98 | 171 |
|
99 |
| -The same approach can be used on read: |
100 |
| - |
101 |
| -.Transaction Conversion on Read |
| 172 | +TransactionResult result = template.getCouchbaseClientFactory().getCluster().transactions().run(ctx -> { |
| 173 | + return template.insertById(Person.class).one(WalterWhite); |
| 174 | +}, options); |
| 175 | +---- |
| 176 | +==== |
| 177 | +.Transaction Access - Reactive |
102 | 178 | ====
|
103 | 179 | [source,java]
|
104 | 180 | ----
|
105 |
| -TransactionGetResult getResult = ctx.get(couchbaseClientFactory.getDefaultCollection(), "doc-id"); |
| 181 | +@Autowired ReactiveCouchbaseTemplate template; |
106 | 182 |
|
107 |
| -CouchbaseDocument source = new CouchbaseDocument(getResult.id()); |
108 |
| -source.setContent(getResult.contentAsObject()); |
109 |
| -Airline read = mappingCouchbaseConverter.read(Airline.class, source); |
| 183 | +TransactionResult result = template.getCouchbaseClientFactory().getCluster().reactive().transactions() |
| 184 | + .run(ctx -> TransactionalSupport.checkForTransactionInThreadLocalStorage().then(Mono.defer(() -> { |
| 185 | + return template.insertById(Person.class).one(WalterWhite); |
| 186 | + })), options).block(); |
110 | 187 | ----
|
111 | 188 | ====
|
112 | 189 |
|
113 |
| -We are also looking into tighter integration of the transaction library into the spring data library |
114 |
| -ecosystem. |
| 190 | + |
0 commit comments