Skip to content

Commit 25fcb14

Browse files
committed
Add support for Couchbase Transactions.
Closes #1145.
1 parent 37648c8 commit 25fcb14

File tree

66 files changed

+1524
-526
lines changed

Some content is hidden

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

66 files changed

+1524
-526
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-SNAPSHOT</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)