DATACOUCH-519 - Docs and small changes for transaction support.

This commit is contained in:
Michael Nitschinger
2020-05-08 15:31:10 +02:00
parent ef6fb9c43b
commit 405df74fa0
11 changed files with 176 additions and 28 deletions

View File

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

View File

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

View File

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

View File

@@ -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;

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -329,7 +329,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
Class<?> mapType = typeMapper.readType(source, type).getType();
Map<Object, Object> map = CollectionFactory.createMap(mapType, source.export().keySet().size());
Map<String, Object> sourceMap = source.getPayload();
Map<String, Object> sourceMap = source.getContent();
for (Map.Entry<String, Object> entry : sourceMap.entrySet()) {
Object key = entry.getKey();

View File

@@ -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<String, Object> payload;
private Map<String, Object> 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<String, Object>();
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<String, Object> export() {
HashMap<String, Object> toExport = new HashMap<String, Object>(payload);
for (Map.Entry<String, Object> entry : payload.entrySet()) {
HashMap<String, Object> toExport = new HashMap<String, Object>(content);
for (Map.Entry<String, Object> 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<String, Object> getPayload() {
return payload;
public Map<String, Object> 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<String, Object> 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 + '}';
}
}

View File

@@ -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;
}
}

View File

@@ -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());