From 405df74fa07c9b420c3311d8c49916e25a6d9d7d Mon Sep 17 00:00:00 2001 From: Michael Nitschinger Date: Fri, 8 May 2020 15:31:10 +0200 Subject: [PATCH] DATACOUCH-519 - Docs and small changes for transaction support. --- src/main/asciidoc/index.adoc | 1 + src/main/asciidoc/transactions.adoc | 114 ++++++++++++++++++ .../couchbase/CouchbaseClientFactory.java | 5 + .../SimpleCouchbaseClientFactory.java | 5 + .../ReactiveInsertByIdOperationSupport.java | 2 +- .../ReactiveReplaceByIdOperationSupport.java | 2 +- .../ReactiveUpsertByIdOperationSupport.java | 2 +- .../convert/MappingCouchbaseConverter.java | 2 +- .../core/mapping/CouchbaseDocument.java | 49 ++++++-- .../core/mapping/CustomConvertersTests.java | 12 +- .../MappingCouchbaseConverterTests.java | 10 +- 11 files changed, 176 insertions(+), 28 deletions(-) create mode 100644 src/main/asciidoc/transactions.adoc diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 00374151..1294b2d1 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -23,6 +23,7 @@ include::{spring-data-commons-docs}/repositories.adoc[] include::repository.adoc[] include::reactiverepository.adoc[] include::template.adoc[] +include::transactions.adoc[] // (daschl) disabled the ansijoins docs since it is being overhauled for 4.x // include::ansijoins.adoc[] :leveloffset: -1 diff --git a/src/main/asciidoc/transactions.adoc b/src/main/asciidoc/transactions.adoc new file mode 100644 index 00000000..d97dfad2 --- /dev/null +++ b/src/main/asciidoc/transactions.adoc @@ -0,0 +1,114 @@ +[[couchbase.transactions]] += Transaction Support + +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. + +== 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. + - 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. + +== Getting Started & Configuration + +The `couchbase-transactions` artifact needs to be included into your `pom.xml` if maven is being used (or equivalent). + + - 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: + +.Transaction Configuration +==== +[source,java] +---- +@Configuration +static class Config extends AbstractCouchbaseConfiguration { + + // Usual Setup + @Override public String getConnectionString() { /* ... */ } + @Override public String getUserName() { /* ... */ } + @Override public String getPassword() { /* ... */ } + @Override public String getBucketName() { /* ... */ } + + @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()); + } + +} +---- +==== + +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: + +.Transaction Access +==== +[source,java] +---- +@Autowired +Transactions transactions; + +@Autowired +CouchbaseClientFactory couchbaseClientFactory; + +public void doSomething() { + transactions.run(ctx -> { + ctx.insert(couchbaseClientFactory.getDefaultCollection(), "id", "content"); + ctx.commit(); + }); +} +---- +==== + +== Object Conversions + +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: + +.Transaction Conversion on Write +==== +[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()); + + ctx.commit(); + }); +} +---- +==== + +The same approach can be used on read: + +.Transaction Conversion on Read +==== +[source,java] +---- +TransactionGetResult getResult = ctx.get(couchbaseClientFactory.getDefaultCollection(), "doc-id"); + +CouchbaseDocument source = new CouchbaseDocument(getResult.id()); +source.setContent(getResult.contentAsObject()); +Airline read = mappingCouchbaseConverter.read(Airline.class, source); +---- +==== + +We are also looking into tighter integration of the transaction library into the spring data library +ecosystem. \ No newline at end of file diff --git a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java index cec54ebd..51781a7d 100644 --- a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java @@ -55,6 +55,11 @@ public interface CouchbaseClientFactory extends Closeable { */ Collection getCollection(String name); + /** + * Provides access to the default collection. + */ + Collection getDefaultCollection(); + /** * Returns a new {@link CouchbaseClientFactory} set to the scope given as an argument. * diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index 2f8d1501..bcb5034b 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java @@ -104,6 +104,11 @@ public class SimpleCouchbaseClientFactory implements CouchbaseClientFactory { return scope.collection(collectionName); } + @Override + public Collection getDefaultCollection() { + return getCollection(null); + } + @Override public PersistenceExceptionTranslator getExceptionTranslator() { return exceptionTranslator; diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index c182ca06..4ce76dae 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -68,7 +68,7 @@ public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOpe return Mono.just(object).flatMap(o -> { CouchbaseDocument converted = template.support().encodeEntity(o); return template.getCollection(collection).reactive() - .insert(converted.getId(), converted.getPayload(), buildInsertOptions()).map(result -> { + .insert(converted.getId(), converted.getContent(), buildInsertOptions()).map(result -> { template.support().applyUpdatedCas(object, result.cas()); return o; }); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index 486bb270..399a4260 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -68,7 +68,7 @@ public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdO return Mono.just(object).flatMap(o -> { CouchbaseDocument converted = template.support().encodeEntity(o); return template.getCollection(collection).reactive() - .replace(converted.getId(), converted.getPayload(), buildReplaceOptions()).map(result -> { + .replace(converted.getId(), converted.getContent(), buildReplaceOptions()).map(result -> { template.support().applyUpdatedCas(object, result.cas()); return o; }); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index 2e4c9ac5..a05c91bf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -68,7 +68,7 @@ public class ReactiveUpsertByIdOperationSupport implements ReactiveUpsertByIdOpe return Mono.just(object).flatMap(o -> { CouchbaseDocument converted = template.support().encodeEntity(o); return template.getCollection(collection).reactive() - .upsert(converted.getId(), converted.getPayload(), buildUpsertOptions()).map(result -> { + .upsert(converted.getId(), converted.getContent(), buildUpsertOptions()).map(result -> { template.support().applyUpdatedCas(object, result.cas()); return o; }); diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index 3448d386..6f5043be 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -329,7 +329,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem Class mapType = typeMapper.readType(source, type).getType(); Map map = CollectionFactory.createMap(mapType, source.export().keySet().size()); - Map sourceMap = source.getPayload(); + Map sourceMap = source.getContent(); for (Map.Entry entry : sourceMap.entrySet()) { Object key = entry.getKey(); diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java index b4a39c58..26a6834d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java @@ -16,6 +16,8 @@ package org.springframework.data.couchbase.core.mapping; +import com.couchbase.client.java.json.JsonObject; + import java.util.HashMap; import java.util.Map; @@ -46,7 +48,7 @@ public class CouchbaseDocument implements CouchbaseStorable { /** * Contains the actual data to be stored. */ - private HashMap payload; + private Map content; /** * Represents the document ID used to identify the document in the bucket. @@ -83,7 +85,7 @@ public class CouchbaseDocument implements CouchbaseStorable { public CouchbaseDocument(final String id, final int expiration) { this.id = id; this.expiration = expiration; - payload = new HashMap(); + content = new HashMap<>(); } /** @@ -96,7 +98,7 @@ public class CouchbaseDocument implements CouchbaseStorable { public final CouchbaseDocument put(final String key, final Object value) { verifyValueType(value); - payload.put(key, value); + content.put(key, value); return this; } @@ -107,7 +109,7 @@ public class CouchbaseDocument implements CouchbaseStorable { * @return the value to which the specified key is mapped, or null if does not contain a mapping for the key. */ public final Object get(final String key) { - return payload.get(key); + return content.get(key); } /** @@ -118,8 +120,8 @@ public class CouchbaseDocument implements CouchbaseStorable { * @return */ public final HashMap export() { - HashMap toExport = new HashMap(payload); - for (Map.Entry entry : payload.entrySet()) { + HashMap toExport = new HashMap(content); + for (Map.Entry entry : content.entrySet()) { if (entry.getValue() instanceof CouchbaseDocument) { toExport.put(entry.getKey(), ((CouchbaseDocument) entry.getValue()).export()); } else if (entry.getValue() instanceof CouchbaseList) { @@ -136,7 +138,7 @@ public class CouchbaseDocument implements CouchbaseStorable { * @return true if it contains a payload for the specified key. */ public final boolean containsKey(final String key) { - return payload.containsKey(key); + return content.containsKey(key); } /** @@ -146,7 +148,7 @@ public class CouchbaseDocument implements CouchbaseStorable { * @return true if it contains the specified value. */ public final boolean containsValue(final Object value) { - return payload.containsValue(value); + return content.containsValue(value); } /** @@ -165,14 +167,14 @@ public class CouchbaseDocument implements CouchbaseStorable { * @return the size of the attributes in this and recursive documents. */ public final int size(final boolean recursive) { - int thisSize = payload.size(); + int thisSize = content.size(); if (!recursive || thisSize == 0) { return thisSize; } int totalSize = thisSize; - for (Object value : payload.values()) { + for (Object value : content.values()) { if (value instanceof CouchbaseDocument) { totalSize += ((CouchbaseDocument) value).size(true); } else if (value instanceof CouchbaseList) { @@ -192,8 +194,29 @@ public class CouchbaseDocument implements CouchbaseStorable { * * @return the underlying payload. */ - public HashMap getPayload() { - return payload; + public Map getContent() { + return content; + } + + /** + * Allows to set the full payload as a map. + * + * @param content the payload to set + * @return this document for chaining purposes. + */ + public CouchbaseDocument setContent(final Map content) { + this.content = content; + return this; + } + + /** + * Allows to set the full payload as a json object for convenience. + * + * @param payload the payload to set + * @return this document for chaining purposes. + */ + public CouchbaseDocument setContent(final JsonObject payload) { + return setContent(payload.toMap()); } /** @@ -271,6 +294,6 @@ public class CouchbaseDocument implements CouchbaseStorable { */ @Override public String toString() { - return "CouchbaseDocument{" + "id=" + id + ", exp=" + expiration + ", payload=" + payload + '}'; + return "CouchbaseDocument{" + "id=" + id + ", exp=" + expiration + ", content=" + content + '}'; } } diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java index 635e014d..f04c5d0e 100644 --- a/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java @@ -60,7 +60,7 @@ public class CustomConvertersTests { CouchbaseDocument doc = new CouchbaseDocument(); converter.write(post, doc); - assertThat(doc.getPayload().get("created")).isEqualTo(date.toString()); + assertThat(doc.getContent().get("created")).isEqualTo(date.toString()); } @Test @@ -71,7 +71,7 @@ public class CustomConvertersTests { converter.afterPropertiesSet(); CouchbaseDocument doc = new CouchbaseDocument(); - doc.getPayload().put("content", 10); + doc.getContent().put("content", 10); Counter loaded = converter.read(Counter.class, doc); assertThat(loaded.content).isEqualTo("even"); } @@ -90,8 +90,8 @@ public class CustomConvertersTests { CouchbaseDocument doc = new CouchbaseDocument(); converter.write(post, doc); - assertThat(doc.getPayload().get("title")).isEqualTo("The Foo of the Bar"); - assertThat(doc.getPayload().get("slug")).isEqualTo("the_foo_of_the_bar"); + assertThat(doc.getContent().get("title")).isEqualTo("The Foo of the Bar"); + assertThat(doc.getContent().get("slug")).isEqualTo("the_foo_of_the_bar"); } @Test @@ -102,7 +102,7 @@ public class CustomConvertersTests { converter.afterPropertiesSet(); CouchbaseDocument doc = new CouchbaseDocument(); - doc.getPayload().put("title", "My Title"); + doc.getContent().put("title", "My Title"); BlogPost loaded = converter.read(BlogPost.class, doc); assertThat(loaded.id).isEqualTo("modified"); @@ -146,7 +146,7 @@ public class CustomConvertersTests { public BlogPost convert(CouchbaseDocument source) { BlogPost post = new BlogPost(); post.id = "modified"; - post.title = source.getPayload().get("title") + "!!"; + post.title = source.getContent().get("title") + "!!"; return post; } } diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java index 3f842d59..325f0801 100644 --- a/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java @@ -399,8 +399,8 @@ public class MappingCouchbaseConverterTests { mapOfValuesDoc.put("val2", value2Str); source.put("mapOfValues", mapOfValuesDoc); - assertThat(valueStr).isEqualTo(((CouchbaseList) converted.getPayload().get("listOfValues")).get(0)); - assertThat(value2Str).isEqualTo(((CouchbaseList) converted.getPayload().get("listOfValues")).get(1)); + assertThat(valueStr).isEqualTo(((CouchbaseList) converted.getContent().get("listOfValues")).get(0)); + assertThat(value2Str).isEqualTo(((CouchbaseList) converted.getContent().get("listOfValues")).get(1)); assertThat(converted.export().toString()).isEqualTo(source.export().toString()); CustomEntity readConverted = converter.read(CustomEntity.class, source); @@ -463,10 +463,10 @@ public class MappingCouchbaseConverterTests { CouchbaseDocument converted = new CouchbaseDocument(); converter.write(entity, converted); - assertThat(converted.getPayload().get("created")).isEqualTo(created.getTime()); - assertThat(converted.getPayload().get("modified")).isEqualTo(modified.getTimeInMillis() / 1000); + assertThat(converted.getContent().get("created")).isEqualTo(created.getTime()); + assertThat(converted.getContent().get("modified")).isEqualTo(modified.getTimeInMillis() / 1000); LocalDateTimeToLongConverter localDateTimeToDateconverter = LocalDateTimeToLongConverter.INSTANCE; - assertThat(converted.getPayload().get("deleted")).isEqualTo(localDateTimeToDateconverter.convert(deleted)); + assertThat(converted.getContent().get("deleted")).isEqualTo(localDateTimeToDateconverter.convert(deleted)); DateEntity read = converter.read(DateEntity.class, converted); assertThat(read.created.getTime()).isEqualTo(created.getTime());