Skip to content

Commit 1679651

Browse files
committed
Add support for Couchbase Transactions.
The fluent operations are common for options that are common to operations with and without transactions. Once there is a transaction(ctx), or an option specific to without-transactions (such as scanConsistency), the interfaces are bifurcated, so that an non-transaction option cannot be applied to a transaction operation, and a transaction(ctx) cannot be applied where a non-transaction option has already been applied. Closes #1145.
1 parent 37648c8 commit 1679651

File tree

78 files changed

+2221
-1205
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+2221
-1205
lines changed

pom.xml

+9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<couchbase>3.2.1</couchbase>
2222
<couchbase.osgi>3.2.1</couchbase.osgi>
2323
<springdata.commons>2.6.0-SNAPSHOT</springdata.commons>
24+
<couchbase-transactions>1.2.1</couchbase-transactions>
2425
<java-module-name>spring.data.couchbase</java-module-name>
2526
</properties>
2627

@@ -37,6 +38,14 @@
3738
</dependencyManagement>
3839

3940
<dependencies>
41+
42+
<dependency>
43+
<groupId>com.couchbase.client</groupId>
44+
<artifactId>couchbase-transactions</artifactId>
45+
<version>${couchbase-transactions}</version>
46+
</dependency>
47+
48+
4049
<dependency>
4150
<groupId>org.springframework</groupId>
4251
<artifactId>spring-context-support</artifactId>

src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ public Scope getScope() {
9797
@Override
9898
public Collection getCollection(final String collectionName) {
9999
final Scope scope = getScope();
100-
if (collectionName == null) {
100+
if (collectionName == null || CollectionIdentifier.DEFAULT_COLLECTION.equals(collectionName)) {
101101
if (!scope.name().equals(CollectionIdentifier.DEFAULT_SCOPE)) {
102-
throw new IllegalStateException("A collectionName must be provided if a non-default scope is used!");
102+
throw new IllegalStateException("A collectionName must be provided if a non-default scope is used");
103103
}
104104
return getBucket().defaultCollection();
105105
}

src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java

+20-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
package org.springframework.data.couchbase.config;
1818

1919
import static com.couchbase.client.java.ClusterOptions.clusterOptions;
20+
import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_MAPPING_CONTEXT;
21+
import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TRANSACTIONS;
2022

23+
import java.time.Duration;
2124
import java.util.Collections;
2225
import java.util.HashSet;
2326
import java.util.Set;
@@ -46,6 +49,7 @@
4649
import org.springframework.util.ClassUtils;
4750
import org.springframework.util.StringUtils;
4851

52+
import com.couchbase.client.core.cnc.Event;
4953
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.DeserializationFeature;
5054
import com.couchbase.client.core.encryption.CryptoManager;
5155
import com.couchbase.client.core.env.Authenticator;
@@ -57,6 +61,10 @@
5761
import com.couchbase.client.java.env.ClusterEnvironment;
5862
import com.couchbase.client.java.json.JacksonTransformers;
5963
import com.couchbase.client.java.json.JsonValueModule;
64+
import com.couchbase.transactions.TransactionDurabilityLevel;
65+
import com.couchbase.transactions.Transactions;
66+
import com.couchbase.transactions.config.TransactionConfig;
67+
import com.couchbase.transactions.config.TransactionConfigBuilder;
6068
import com.fasterxml.jackson.databind.ObjectMapper;
6169

6270
/**
@@ -122,7 +130,7 @@ protected Authenticator authenticator() {
122130
* @param couchbaseCluster the cluster reference from the SDK.
123131
* @return the initialized factory.
124132
*/
125-
@Bean
133+
@Bean(name = BeanNames.COUCHBASE_CLIENT_FACTORY)
126134
public CouchbaseClientFactory couchbaseClientFactory(final Cluster couchbaseCluster) {
127135
return new SimpleCouchbaseClientFactory(couchbaseCluster, getBucketName(), getScopeName());
128136
}
@@ -283,7 +291,7 @@ public TranslationService couchbaseTranslationService() {
283291
*
284292
* @throws Exception on Bean construction failure.
285293
*/
286-
@Bean
294+
@Bean(COUCHBASE_MAPPING_CONTEXT)
287295
public CouchbaseMappingContext couchbaseMappingContext(CustomConversions customConversions) throws Exception {
288296
CouchbaseMappingContext mappingContext = new CouchbaseMappingContext();
289297
mappingContext.setInitialEntitySet(getInitialEntitySet());
@@ -312,6 +320,16 @@ public ObjectMapper couchbaseObjectMapper() {
312320
return mapper;
313321
}
314322

323+
@Bean(COUCHBASE_TRANSACTIONS)
324+
public Transactions getTransactions(Cluster cluster) {
325+
return Transactions.create(cluster, getTransactionConfig());
326+
}
327+
328+
TransactionConfig getTransactionConfig() {
329+
return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR)
330+
.expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.NONE).build();
331+
}
332+
315333
/**
316334
* Configure whether to automatically create indices for domain types by deriving the from the entity or not.
317335
*/

src/main/java/org/springframework/data/couchbase/config/BeanNames.java

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class BeanNames {
3434

3535
public static final String COUCHBASE_CUSTOM_CONVERSIONS = "couchbaseCustomConversions";
3636

37+
public static final String COUCHBASE_TRANSACTIONS = "couchbaseTransactions";
38+
3739
/**
3840
* The name for the bean that stores custom mapping between repositories and their backing couchbaseOperations.
3941
*/
@@ -59,4 +61,7 @@ public class BeanNames {
5961
* The name for the bean that will handle reactive audit trail marking of entities.
6062
*/
6163
public static final String REACTIVE_COUCHBASE_AUDITING_HANDLER = "reactiveCouchbaseAuditingHandler";
64+
65+
public static final String COUCHBASE_CLIENT_FACTORY = "couchbaseClientFactory";
66+
6267
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package org.springframework.data.couchbase.core;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
import org.springframework.context.ApplicationContext;
6+
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
7+
import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver;
8+
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
9+
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
10+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
11+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
12+
import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent;
13+
import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent;
14+
import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation;
15+
import org.springframework.data.couchbase.repository.support.TransactionResultHolder;
16+
import org.springframework.data.mapping.PersistentPropertyAccessor;
17+
import org.springframework.data.mapping.context.MappingContext;
18+
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
19+
20+
import com.couchbase.client.core.error.CouchbaseException;
21+
22+
public abstract class AbstractTemplateSupport {
23+
24+
final CouchbaseConverter converter;
25+
final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext;
26+
final TranslationService translationService;
27+
ApplicationContext applicationContext;
28+
static final Logger LOG = LoggerFactory.getLogger(AbstractTemplateSupport.class);
29+
30+
public AbstractTemplateSupport(CouchbaseConverter converter, TranslationService translationService) {
31+
this.converter = converter;
32+
this.mappingContext = converter.getMappingContext();
33+
this.translationService = translationService;
34+
}
35+
36+
abstract ReactiveCouchbaseTemplate getReactiveTemplate();
37+
38+
public <T> T decodeEntityBase(String id, String source, long cas, Class<T> entityClass,
39+
TransactionResultHolder txResultHolder) {
40+
41+
final CouchbaseDocument converted = new CouchbaseDocument(id);
42+
converted.setId(id);
43+
CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(entityClass);
44+
if (cas != 0 && persistentEntity.getVersionProperty() != null) {
45+
converted.put(persistentEntity.getVersionProperty().getName(), cas);
46+
}
47+
48+
T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted));
49+
final ConvertingPropertyAccessor<T> accessor = getPropertyAccessor(readEntity);
50+
51+
if (persistentEntity.getVersionProperty() != null) {
52+
accessor.setProperty(persistentEntity.getVersionProperty(), cas);
53+
}
54+
if (persistentEntity.transactionResultProperty() != null) {
55+
accessor.setProperty(persistentEntity.transactionResultProperty(), txResultHolder);
56+
}
57+
N1qlJoinResolver.handleProperties(persistentEntity, accessor, getReactiveTemplate(), id);
58+
return accessor.getBean();
59+
}
60+
61+
public <T> T applyResultBase(T entity, CouchbaseDocument converted, Object id, long cas,
62+
TransactionResultHolder txResultHolder) {
63+
ConvertingPropertyAccessor<Object> accessor = getPropertyAccessor(entity);
64+
65+
final CouchbasePersistentEntity<?> persistentEntity = converter.getMappingContext()
66+
.getRequiredPersistentEntity(entity.getClass());
67+
68+
final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty();
69+
if (idProperty != null) {
70+
accessor.setProperty(idProperty, id);
71+
}
72+
73+
final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty();
74+
if (versionProperty != null) {
75+
accessor.setProperty(versionProperty, cas);
76+
}
77+
78+
final CouchbasePersistentProperty transactionResultProperty = persistentEntity.transactionResultProperty();
79+
if (transactionResultProperty != null) {
80+
accessor.setProperty(transactionResultProperty, txResultHolder);
81+
}
82+
maybeEmitEvent(new AfterSaveEvent(accessor.getBean(), converted));
83+
return (T) accessor.getBean();
84+
85+
}
86+
87+
public Long getCas(final Object entity) {
88+
final ConvertingPropertyAccessor<Object> accessor = getPropertyAccessor(entity);
89+
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass());
90+
final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty();
91+
92+
long cas = 0;
93+
if (versionProperty != null) {
94+
Object casObject = accessor.getProperty(versionProperty);
95+
if (casObject instanceof Number) {
96+
cas = ((Number) casObject).longValue();
97+
}
98+
}
99+
return cas;
100+
}
101+
102+
public String getJavaNameForEntity(final Class<?> clazz) {
103+
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(clazz);
104+
MappingCouchbaseEntityInformation<?, Object> info = new MappingCouchbaseEntityInformation<>(persistentEntity);
105+
return info.getJavaType().getName();
106+
}
107+
108+
<T> ConvertingPropertyAccessor<T> getPropertyAccessor(final T source) {
109+
CouchbasePersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(source.getClass());
110+
PersistentPropertyAccessor<T> accessor = entity.getPropertyAccessor(source);
111+
return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService());
112+
}
113+
114+
public <T> TransactionResultHolder getTxResultHolder(T source) {
115+
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(source.getClass());
116+
final CouchbasePersistentProperty transactionResultProperty = persistentEntity.transactionResultProperty();
117+
if (transactionResultProperty == null) {
118+
throw new CouchbaseException("the entity class " + source.getClass()
119+
+ " does not have a property required for transactions:\n\t@TransactionResult TransactionResultHolder txResultHolder");
120+
}
121+
return getPropertyAccessor(source).getProperty(transactionResultProperty, TransactionResultHolder.class);
122+
}
123+
124+
public void maybeEmitEvent(CouchbaseMappingEvent<?> event) {
125+
if (canPublishEvent()) {
126+
try {
127+
this.applicationContext.publishEvent(event);
128+
} catch (Exception e) {
129+
LOG.warn("{} thrown during {}", e, event);
130+
throw e;
131+
}
132+
} else {
133+
LOG.info("maybeEmitEvent called, but CouchbaseTemplate not initialized with applicationContext");
134+
}
135+
136+
}
137+
138+
private boolean canPublishEvent() {
139+
return this.applicationContext != null;
140+
}
141+
}

0 commit comments

Comments
 (0)