DATACOUCH-519 - Docs and small changes for transaction support.
This commit is contained in:
@@ -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
|
||||
|
||||
114
src/main/asciidoc/transactions.adoc
Normal file
114
src/main/asciidoc/transactions.adoc
Normal 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.
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user