Skip to content

Update transactions documentation for native transaction support. #1513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,29 @@ Building the documentation builds also the project without running tests.

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

=== Building and staging reference documentation for review

[source,bash]
----
export MY_GIT_USER=<github-user>
mvn generate-resources
docs=`pwd`/target/site/reference/html
pushd /tmp
mkdir $$
cd $$
# see https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site
# this examples uses a repository named "staged"
git clone [email protected]:${MY_GIT_USER}/staged.git -b gh-pages
cd staged
cp -R $docs/* .
git add .
git commit --message "stage for review"
git push origin gh-pages
popd
----

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

== Examples

* https://github.com/spring-projects/spring-data-examples/[Spring Data Examples] contains example projects that explain specific features in more detail.
Expand Down
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,20 @@
<artifactId>maven-assembly-plugin</artifactId>
</plugin>
<plugin>
<!-- generate asciidoc to stage for review -->
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<executions>
<execution>
<phase>generate-resources</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<outputDirectory>target/site/reference/html</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.mysema.maven</groupId>
Expand Down
202 changes: 133 additions & 69 deletions src/main/asciidoc/transactions.adoc
Original file line number Diff line number Diff line change
@@ -1,114 +1,178 @@
[[couchbase.transactions]]
= Transaction Support
= Couchbase Transactions

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.
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.

== Requirements

- Couchbase Server 6.5 or above.
- Couchbase Java client 3.0.0 or above. It is recommended to follow the transitive dependency for the transactions library from maven.
- Couchbase Server 6.6.1 or aabove.
- Spring Data Couchbase 5.0.0-M5 or above.
- 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.
- Set spring.main.allow-bean-definition-overriding=true either in application.properties or as a SpringApplicationBuilder property.

== Getting Started & Configuration

The `couchbase-transactions` artifact needs to be included into your `pom.xml` if maven is being used (or equivalent).
== Overview
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.

- Group: `com.couchbase.client`
- Artifact: `couchbase-transactions`
- Version: latest one, i.e. `1.0.0`

Once it is included in your project, you need to create a single `Transactions` object. Conveniently, it can be part of
your spring data couchbase `AbstractCouchbaseConfiguration` implementation:
== Getting Started & Configuration

.Transaction Configuration
Couchbase Transactions are normally leveraged with a method annotated with @Transactional.
The @Transactional operator is implemented with the CouchbaseTransactionManager which is supplied as a bean in the AbstractCouchbaseConfiguration.
Couchbase Transactions can be used without defining a service class by using CouchbaseTransactionOperator which is also supplied as a bean in AbtractCouchbaseConfiguration.
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]

== Transactions with @Transactional

@Transactional defines as transactional a method or all methods on a class.

When this annotation is declared at the class level, it applies as a default
to all methods of the declaring class and its subclasses.

=== Attribute Semantics

In this release, the Couchbase Transactions ignores the rollback attributes.
The transaction isolation level is read-committed;

.Transaction Configuration and Use by @Transactional
====
.The Configuration
[source,java]
----
@Configuration
@EnableCouchbaseRepositories("<parent-dir-of-repository-interfaces>")
@EnableReactiveCouchbaseRepositories("<parent-dir-of-repository-interfaces>")
@EnableTransactionManagement // <1>
static class Config extends AbstractCouchbaseConfiguration {

// Usual Setup
@Override public String getConnectionString() { /* ... */ }
@Override public String getUserName() { /* ... */ }
@Override public String getPassword() { /* ... */ }
@Override public String getBucketName() { /* ... */ }
// Usual Setup
@Override public String getConnectionString() { /* ... */ }
@Override public String getUserName() { /* ... */ }
@Override public String getPassword() { /* ... */ }
@Override public String getBucketName() { /* ... */ }

// Customization of transaction behavior is via the configureEnvironment() method
@Override protected void configureEnvironment(final Builder builder) {
builder.transactionsConfig(
TransactionsConfig.builder().timeout(Duration.ofSeconds(30)));
}
}
----
.The Transactional Service Class
Note that the body of @Transactional methods can be re-executed if the transaction fails.
It is imperative that everthing in the method body be idempotent.
[source,java]
----
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

final CouchbaseOperations personOperations;
final ReactiveCouchbaseOperations reactivePersonOperations;

@Service // <2>
public class PersonService {

final CouchbaseOperations operations;
final ReactiveCouchbaseOperations reactiveOperations;

public PersonService(CouchbaseOperations ops, ReactiveCouchbaseOperations reactiveOps) {
operations = ops;
reactiveOperations = reactiveOps;
}

// no annotation results in this method being executed not in a transaction
public Person save(Person p) {
return operations.save(p);
}

@Transactional
public Person changeFirstName(String id, String newFirstName) {
Person p = operations.findById(Person.class).one(id); // <3>
return operations.replaceById(Person.class).one(p.withFirstName(newFirstName);
}

@Bean
public Transactions transactions(final Cluster couchbaseCluster) {
return Transactions.create(couchbaseCluster, TransactionConfigBuilder.create()
// The configuration can be altered here, but in most cases the defaults are fine.
.build());
}
@Transactional
public Mono<Person> reactiveChangeFirstName(String id, String newFirstName) {
return personOperationsRx.findById(Person.class).one(person.id())
.flatMap(p -> personOperationsRx.replaceById(Person.class).one(p.withFirstName(newFirstName)));
}

}
----
[source,java]
.Using the @Transactional Service.
----
@Autowired PersonService personService; // <4>

Person walterWhite = new Person( "Walter", "White");
Person p = personService.save(walterWhite); // this is not a transactional method
...
Person renamedPerson = personService.changeFirstName(walterWhite.getId(), "Ricky"); // <5>
----
Functioning of the @Transactional method annotation requires
[start=1]
. the configuration class to be annotated with @EnableTransactionManagement;
. the service object with the annotated methods must be annotated with @Service;
. the body of the method is executed in a transaction.
. the service object with the annotated methods must be obtained via @Autowired.
. the call to the method must be made from a different class than service because calling an annotated
method from the same class will not invoke the Method Interceptor that does the transaction processing.
====

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

.Transaction Access
CouchbaseTransactionalOperator can be used to construct a transaction in-line without creating a service class that uses @Transactional.
CouchbaseTransactionalOperator is available as a bean and can be instantiated with @Autowired.
If creating one explicitly, it must be created with CouchbaseTransactionalOperator.create(manager) (NOT TransactionalOperator.create(manager)).

.Transaction Access Using TransactionalOperator.execute()
====
[source,java]
----
@Autowired
Transactions transactions;

@Autowired
CouchbaseClientFactory couchbaseClientFactory;
@Autowired TransactionalOperator txOperator;
@Autowired ReactiveCouchbaseTemplate reactiveCouchbaseTemplate;

public void doSomething() {
transactions.run(ctx -> {
ctx.insert(couchbaseClientFactory.getDefaultCollection(), "id", "content");
ctx.commit();
});
}
Flux<Person> result = txOperator.execute((ctx) ->
reactiveCouchbaseTemplate.findById(Person.class).one(person.id())
.flatMap(p -> reactiveCouchbaseTemplate.replaceById(Person.class).one(p.withFirstName("Walt")))
);
----
====

== Object Conversions
== Transactions Directly with the SDK
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So as in previous review

  1. I think this should be front and centre as the Couchbase recommended way to do transactions with Spring, and mentioned right at the top. Full power, no limitations.
  2. It's too short :) We don't want to C&P the docs but we can mention what's cool about this. We can say that you can use any Spring operations directly inside the lambda, that you can mix and match those with the regular transactional API, that sort of thing. Let's big ourselves up a bit :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link to the transactions doc is there.

We can say that you can use any Spring operations directly inside the lambda

Yes - I seem to have copied only the samples and lost the accompanying text. Will revisit that.


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

.Transaction Conversion on Write
Please see the https://docs.couchbase.com/java-sdk/current/howtos/distributed-acid-transactions-from-the-sdk.html[Reference Documentation]

.Transaction Access - Blocking
====
[source,java]
----
@Autowired
MappingCouchbaseConverter mappingCouchbaseConverter;

public void doSomething() {
transactions.run(ctx -> {

Airline airline = new Airline("demo-airline", "at");
CouchbaseDocument target = new CouchbaseDocument();
mappingCouchbaseConverter.write(airline, target);

ctx.insert(couchbaseClientFactory.getDefaultCollection(), target.getId(), target.getContent());
@Autowired CouchbaseTemplate couchbaseTemplate;

ctx.commit();
});
}
TransactionResult result = couchbaseTemplate.getCouchbaseClientFactory().getCluster().transactions().run(ctx -> {
Person p = couchbaseTemplate.findById(Person.class).one(personId);
couchbaseTemplate.replaceById(Person.class).one(p.withFirstName("Walt"));
});
----
====

The same approach can be used on read:

.Transaction Conversion on Read
.Transaction Access - Reactive
====
[source,java]
----
TransactionGetResult getResult = ctx.get(couchbaseClientFactory.getDefaultCollection(), "doc-id");
@Autowired ReactiveCouchbaseTemplate reactiveCouchbaseTemplate;

CouchbaseDocument source = new CouchbaseDocument(getResult.id());
source.setContent(getResult.contentAsObject());
Airline read = mappingCouchbaseConverter.read(Airline.class, source);
Mono<TransactionResult> result = reactiveCouchbaseTemplate.getCouchbaseClientFactory().getCluster().reactive().transactions()
.run(ctx ->
reactiveCouchbaseTemplate.findById(Person.class).one(personId)
.flatMap(p -> reactiveCouchbaseTemplate.replaceById(Person.class).one(p.withFirstName("Walt")))
);
----
====

We are also looking into tighter integration of the transaction library into the spring data library
ecosystem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the end of the docs now?
There's an awful lot of feedback from before that hasn't been incorporated... including very very crucial things like talking about the retry model. Could you please take another pass on my previous review? Anything you disagree with and don't want to incorporate, please comment on it and we can iterate further.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look at the review comments again.
I'll try to describe the retry model. But on the one hand there are a bunch of comments about not getting bogged down in the weeds and implementation details, and on the other hand "there's no explanation for xyz".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, yes - there were niche concerns that were extremely detailed or didn't apply to us at all (it's much better now), and crucial aspects that weren't described.

Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionInterceptor;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

Expand Down Expand Up @@ -330,6 +331,16 @@ CouchbaseCallbackTransactionManager couchbaseTransactionManager(CouchbaseClientF
return new CouchbaseCallbackTransactionManager(clientFactory);
}

/**
* The default transaction template manager.
*
* @param couchbaseTransactionManager
* @return
*/
@Bean(BeanNames.COUCHBASE_TRANSACTION_TEMPLATE)
TransactionTemplate couchbaseTransactionTemplate(CouchbaseCallbackTransactionManager couchbaseTransactionManager) {
return new TransactionTemplate(couchbaseTransactionManager);
}
/**
* The default TransactionalOperator.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,7 @@ public class BeanNames {

public static final String COUCHBASE_TRANSACTION_MANAGER = "couchbaseTransactionManager";

public static final String COUCHBASE_TRANSACTION_TEMPLATE = "couchbaseTransactionTemplate";

public static final String COUCHBASE_TRANSACTIONAL_OPERATOR = "couchbaseTransactionalOperator";
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public void shouldRollbackAfterException() {

@Test
public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() {
assertThrowsWithCause(() -> personService.declarativeSavePersonErrors(WalterWhite).blockLast(),
assertThrowsWithCause(() -> personService.declarativeSavePersonErrors(WalterWhite).block(),
TransactionSystemUnambiguousException.class, SimulateFailureException.class);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.data.couchbase.util.JavaIntegrationTests;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.EnableTransactionManagement;
Expand Down Expand Up @@ -114,9 +113,7 @@ public void upsertById() {
});
}

@Service
@Component
@EnableTransactionManagement
@Service // this will work in the unit tests even without @Service because of explicit loading by @SpringJUnitConfig
static class PersonService {
final CouchbaseOperations personOperations;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.data.couchbase.util.JavaIntegrationTests;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.EnableTransactionManagement;
Expand Down Expand Up @@ -100,9 +99,7 @@ public void supportedIsolation() {
personService.supportedIsolation();
}

@Service
@Component
@EnableTransactionManagement
@Service // this will work in the unit tests even without @Service because of explicit loading by @SpringJUnitConfig
static class PersonService {
final CouchbaseOperations ops;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.data.couchbase.util.JavaIntegrationTests;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.IllegalTransactionStateException;
Expand Down Expand Up @@ -285,9 +284,7 @@ public void callDefaultThatCallsDefaultRetries() {
assertEquals(3, attempts.get());
}

@Service
@Component
@EnableTransactionManagement
@Service // this will work in the unit tests even without @Service because of explicit loading by @SpringJUnitConfig
static class PersonService {
final CouchbaseOperations ops;

Expand Down
Loading