diff --git a/couchbase/pom.xml b/couchbase/pom.xml
index d817a9a5..37eee305 100644
--- a/couchbase/pom.xml
+++ b/couchbase/pom.xml
@@ -18,6 +18,7 @@
example
reactive
+ transactions
util
diff --git a/couchbase/transactions/README.md b/couchbase/transactions/README.md
new file mode 100644
index 00000000..f77ad7d3
--- /dev/null
+++ b/couchbase/transactions/README.md
@@ -0,0 +1,17 @@
+# Getting Started
+
+### Running the Sample
+
+* `$> docker run -d --name db -p 8091-8097:8091-8097 -p 11210:11210 -p 11207:11207 -p 18091-18095:18091-18095 -p 18096:18096 -p 18097:18097 couchbase`
+* Create new _cluster_ via `http://localhost:8091/`
+* Create new [bucket](http://localhost:8091/ui/index.html#/buckets) named _travel-sample_.
+*
+
+### Reference Documentation
+For further reference, please consider the following sections:
+
+* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
+* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.5.2/maven-plugin/reference/html/)
+* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.5.2/maven-plugin/reference/html/#build-image)
+* [Spring Data Couchbase](https://docs.spring.io/spring-boot/docs/2.5.2/reference/htmlsingle/#boot-features-couchbase)
+
diff --git a/couchbase/transactions/pom.xml b/couchbase/transactions/pom.xml
new file mode 100644
index 00000000..bf032df9
--- /dev/null
+++ b/couchbase/transactions/pom.xml
@@ -0,0 +1,80 @@
+
+
+ 4.0.0
+
+ spring-data-couchbase-transactions
+ Spring Data Couchbase - Transaction example
+ Transactions Demo project for Spring Data Couchbase
+
+
+ org.springframework.data.examples
+ spring-data-couchbase-examples
+ 2.0.0.BUILD-SNAPSHOT
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-couchbase
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ ${project.groupId}
+ spring-data-couchbase-example-utils
+ ${project.version}
+ test
+
+
+ org.springframework.data.examples
+ spring-data-couchbase-example-utils
+ 2.0.0.BUILD-SNAPSHOT
+ test
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+ spring-libs-snapshot
+ https://repo.spring.io/libs-snapshot
+
+
+ sonatype-snapshot
+ https://oss.sonatype.org/content/repositories/snapshots
+
+ true
+
+
+ false
+
+
+
+
+
+
+ spring-snapshots
+ https://repo.spring.io/snapshot
+
+
+ spring-milestones
+ https://repo.spring.io/milestone
+
+
+
+
diff --git a/couchbase/transactions/src/main/java/com/example/demo/AirlineGates.java b/couchbase/transactions/src/main/java/com/example/demo/AirlineGates.java
new file mode 100644
index 00000000..83623427
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/AirlineGates.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2012-2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.Version;
+import org.springframework.data.couchbase.core.index.QueryIndexed;
+import org.springframework.data.couchbase.core.mapping.Document;
+
+/**
+ * @author Michael Reiche
+ */
+@Document
+public class AirlineGates {
+ @Id String id;
+ @Version Long version;
+ @QueryIndexed String name;
+ String iata;
+ Long gates;
+
+ @PersistenceConstructor
+ public AirlineGates(String id, String name, String iata, Long gates) {
+ this.id = id;
+ this.name = name;
+ this.iata = iata;
+ this.gates = gates;
+ }
+
+ public String getId() {
+ return id;
+ }
+ public Long getGates() {
+ return gates;
+ }
+
+ public String toString(){
+ StringBuffer sb=new StringBuffer();
+ sb.append("{");
+ sb.append("\"id\":"+id);
+ sb.append(", \"name\":"+name);
+ sb.append(", \"iata\":"+iata);
+ sb.append(", \"gates\":"+gates);
+ sb.append("}");
+
+ return sb.toString();
+ }
+
+}
diff --git a/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesRepository.java b/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesRepository.java
new file mode 100644
index 00000000..49b974ed
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesRepository.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.demo;
+
+import org.springframework.data.couchbase.repository.CouchbaseRepository;
+import org.springframework.data.couchbase.repository.DynamicProxyable;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author Michael Reiche
+ */
+@Repository
+public interface AirlineGatesRepository
+ extends CouchbaseRepository, DynamicProxyable {
+
+}
diff --git a/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesService.java b/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesService.java
new file mode 100644
index 00000000..273e3e6f
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesService.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.data.couchbase.core.CouchbaseTemplate;
+import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
+import org.springframework.data.couchbase.core.TransactionalSupport;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * @author Michael Reiche
+ */
+@Service
+public class AirlineGatesService {
+ CouchbaseTemplate template;
+ ReactiveCouchbaseTemplate reactiveTemplate;
+ AirlineGatesRepository airlineGatesRepository;
+
+ public AirlineGatesService(CouchbaseTemplate template, AirlineGatesRepository airlineGatesRepository) {
+ this.template = template;
+ this.reactiveTemplate = template.reactive();
+ this.airlineGatesRepository = airlineGatesRepository;
+ }
+
+ @Transactional
+ public void transferGates(String fromId, String toId, int gatesToTransfer, RuntimeException exceptionToThrow) {
+ AirlineGates fromAirlineGates = template.findById(AirlineGates.class).one(fromId);
+ AirlineGates toAirlineGates = template.findById(AirlineGates.class).one(toId);
+ toAirlineGates.gates += gatesToTransfer;
+ fromAirlineGates.gates -= gatesToTransfer;
+ template.save(fromAirlineGates);
+ if (exceptionToThrow != null) {
+ throw exceptionToThrow;
+ }
+ template.save(toAirlineGates);
+ }
+
+ @Transactional
+ public void transferGatesRepo(String fromId, String toId, int gatesToTransfer, RuntimeException exceptionToThrow) {
+ AirlineGates fromAirlineGates = airlineGatesRepository.findById(fromId).orElse(null);
+ AirlineGates toAirlineGates = airlineGatesRepository.findById(toId).orElse(null);
+ toAirlineGates.gates += gatesToTransfer;
+ fromAirlineGates.gates -= gatesToTransfer;
+ airlineGatesRepository.save(fromAirlineGates);
+ if (exceptionToThrow != null) {
+ throw exceptionToThrow;
+ }
+ airlineGatesRepository.save(toAirlineGates);
+ }
+
+ // The @Transactional annotation results in the method of the proxy for the service executing this in a transaction
+ @Transactional
+ public Mono transferGatesReactive(String fromId, String toId, int gatesToTransfer,
+ RuntimeException exceptionToThrow) {
+ return Mono.deferContextual(ctx -> {
+ AirlineGates fromAirlineGates = template.findById(AirlineGates.class).one(fromId);
+ AirlineGates toAirlineGates = template.findById(AirlineGates.class).one(toId);
+ toAirlineGates.gates += gatesToTransfer;
+ fromAirlineGates.gates -= gatesToTransfer;
+ template.save(fromAirlineGates);
+ if (exceptionToThrow != null) {
+ throw exceptionToThrow;
+ }
+ return reactiveTemplate.save(toAirlineGates).then();
+ });
+ }
+
+ // This does not have the @Transactional annotation therefore is not executed in a transaction
+ public AirlineGates save(AirlineGates airlineGates) {
+ return template.save(airlineGates);
+ }
+
+ // This does not have the @Transactional annotation therefore is not executed in a transaction
+ public AirlineGates findById(String id) {
+ return template.findById(AirlineGates.class).one(id);
+ }
+
+ // This does not have the @Transactional annotation therefore is not executed in a transaction
+ public AirlineGates saveRepo(AirlineGates airlineGates) {
+ return airlineGatesRepository.save(airlineGates);
+ }
+
+ // This does not have the @Transactional annotation therefore is not executed in a transaction
+ public AirlineGates findByIdRepo(String id) {
+ return airlineGatesRepository.findById(id).orElse(null);
+ }
+}
diff --git a/couchbase/transactions/src/main/java/com/example/demo/CmdRunner.java b/couchbase/transactions/src/main/java/com/example/demo/CmdRunner.java
new file mode 100644
index 00000000..20d02408
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/CmdRunner.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.data.couchbase.core.CouchbaseTemplate;
+import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+
+/**
+ * Components of the type CommandLineRunner are called right after the application start up. So the method *run* is
+ * called as soon as the application starts.
+ *
+ * @author Michael Reiche
+ */
+@Component
+public class CmdRunner implements CommandLineRunner {
+
+ @Autowired CouchbaseTemplate template;
+ @Autowired AirlineGatesService airlineGatesService;
+
+ //@Override
+ public void run(String... strings) {
+
+ try { // remove leftovers from previous run
+ template.removeById(AirlineGates.class).one("1");
+ } catch (Exception e) {}
+ try {
+ template.removeById(AirlineGates.class).one("2");
+ } catch (Exception e) {}
+
+ AirlineGates airlineGates1 = new AirlineGates("1", "American Airlines", "JFK", Long.valueOf(200)); // 1
+ AirlineGates airlineGates2 = new AirlineGates("2", "Lufthansa", "JFK", Long.valueOf(200));
+ AirlineGates saved1 = airlineGatesService.save(airlineGates1);
+ AirlineGates saved2 = airlineGatesService.save(airlineGates2);
+ AirlineGates found_a_1 = airlineGatesService.findById(saved1.getId()); // 2
+ AirlineGates found_a_2 = airlineGatesService.findById(saved2.getId());
+ System.err.println("initialized airlines");
+ System.err.println(" found before transferGates: " + found_a_1);
+ System.err.println(" found before transferGates: " + found_a_2);
+ // move 50 gates from airline1 to airline2
+ int gatesToTransfer = 50;
+ System.err.println("===============================================================");
+ System.err.println("this transferGates attempt will succeed. transferring " + gatesToTransfer);
+
+ airlineGatesService.transferGates(airlineGates1.getId(), airlineGates2.getId(), gatesToTransfer, null); // 3
+
+ AirlineGates found_b_1 = airlineGatesService.findById(airlineGates1.getId());
+ AirlineGates found_b_2 = airlineGatesService.findById(airlineGates2.getId());
+ System.err.println(" found after transferGates: " + found_b_1); // 4
+ System.err.println(" found after transferGates: " + found_b_2);
+ Assert.isTrue(found_b_1.getGates().equals(found_a_1.getGates() - gatesToTransfer), "should have transferred");
+ Assert.isTrue(found_b_2.getGates().equals(found_a_1.getGates() + gatesToTransfer), "should have transferred");
+ System.err.println("===============================================================");
+ gatesToTransfer = 44;
+ System.err.println("this transferGates attempt will fail. transferring " + gatesToTransfer);
+ // attempt to move 44 gates from airline1 to airline2, but it fails.
+ try {
+
+ airlineGatesService.transferGates(airlineGates1.getId(), airlineGates2.getId(), 44, new SimulateErrorException());
+
+ } catch (RuntimeException rte) {
+ if (!(rte instanceof TransactionSystemUnambiguousException) && rte != null
+ && rte.getCause() instanceof SimulateErrorException) {
+ throw rte;
+ }
+ System.err.println(" got exception " + rte);
+ }
+ AirlineGates found_c_1 = airlineGatesService.findById(airlineGates1.getId());
+ AirlineGates found_c_2 = airlineGatesService.findById(airlineGates2.getId());
+ System.err.println(" found after transferGates: " + found_c_1);
+ System.err.println(" found after transferGates: " + found_c_2);
+ Assert.isTrue(found_c_1.getGates().equals(found_b_1.getGates()), "should be same as previous");
+ Assert.isTrue(found_c_2.getGates().equals(found_b_2.getGates()), "should be same as previous");
+ System.err.println("===============================================================");
+ gatesToTransfer = 44;
+ System.err.println("this transferGates attempt will succeed. transferring " + gatesToTransfer);
+ try {
+
+ airlineGatesService.transferGatesReactive(airlineGates1.getId(), airlineGates2.getId(), gatesToTransfer, null)
+ .block();
+
+ } catch (RuntimeException rte) {
+ if (!(rte instanceof TransactionSystemUnambiguousException) && rte != null
+ && rte.getCause() instanceof SimulateErrorException) {
+ throw rte;
+ }
+ System.err.println(" got exception " + rte);
+ }
+ AirlineGates found_d_1 = airlineGatesService.findById(airlineGates1.getId());
+ AirlineGates found_d_2 = airlineGatesService.findById(airlineGates2.getId());
+ System.err.println(" found after transferGates: " + found_d_1);
+ System.err.println(" found after transferGates: " + found_d_2);
+ Assert.isTrue(found_d_1.getGates().equals(found_c_1.getGates() - gatesToTransfer), "should have transferred");
+ Assert.isTrue(found_d_2.getGates().equals(found_c_2.getGates() + gatesToTransfer), "should have transferred");
+ System.err.println("===============================================================");
+ }
+
+ static class SimulateErrorException extends RuntimeException {}
+}
diff --git a/couchbase/transactions/src/main/java/com/example/demo/Config.java b/couchbase/transactions/src/main/java/com/example/demo/Config.java
new file mode 100644
index 00000000..790e45fb
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/Config.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import com.couchbase.client.core.msg.kv.DurabilityLevel;
+import com.couchbase.client.java.env.ClusterEnvironment;
+import com.couchbase.client.java.transactions.config.TransactionsConfig;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
+import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * @author Michael Reiche
+ */
+@Configuration
+@EnableCouchbaseRepositories({"com.example.demo"})
+@EnableTransactionManagement
+public class Config extends AbstractCouchbaseConfiguration {
+ @Override
+ public String getConnectionString() {
+ return "127.0.0.1";
+ }
+
+ @Override
+ public String getUserName() {
+ return "Administrator";
+ }
+
+ @Override
+ public String getPassword() {
+ return "password";
+ }
+
+ @Override
+ public String getBucketName() {
+ return "travel-sample";
+ }
+
+ @Override
+ public void configureEnvironment(ClusterEnvironment.Builder builder){
+ builder.transactionsConfig(TransactionsConfig.durabilityLevel(DurabilityLevel.NONE));
+ }
+
+
+}
diff --git a/couchbase/transactions/src/main/java/com/example/demo/DemoApplication.java b/couchbase/transactions/src/main/java/com/example/demo/DemoApplication.java
new file mode 100644
index 00000000..5c401333
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/DemoApplication.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import java.util.Properties;
+
+/**
+ * @author Michael Reiche
+ */
+@SpringBootApplication
+public class DemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run( DemoApplication.class, args );
+ }
+
+}
diff --git a/couchbase/transactions/src/main/resources/application.properties b/couchbase/transactions/src/main/resources/application.properties
new file mode 100644
index 00000000..e439ebd8
--- /dev/null
+++ b/couchbase/transactions/src/main/resources/application.properties
@@ -0,0 +1 @@
+spring.main.allow-bean-definition-overriding=true
diff --git a/couchbase/transactions/src/main/resources/logback.xml b/couchbase/transactions/src/main/resources/logback.xml
new file mode 100644
index 00000000..a0a452dc
--- /dev/null
+++ b/couchbase/transactions/src/main/resources/logback.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+ %d %5p %40.40c:%4L - %m%n
+
+
+
+
+
+
+
+
+
+
+
+ "
+
+ "
+
+
+
diff --git a/couchbase/transactions/src/test/java/com/example/demo/DemoApplicationTests.java b/couchbase/transactions/src/test/java/com/example/demo/DemoApplicationTests.java
new file mode 100644
index 00000000..f9669e36
--- /dev/null
+++ b/couchbase/transactions/src/test/java/com/example/demo/DemoApplicationTests.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import example.springdata.couchbase.util.EnabledIfCouchbaseAvailable;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * @author Michael Reiche
+ * @author Christoph Strobl
+ */
+@EnabledIfCouchbaseAvailable
+@SpringBootTest
+class DemoApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+}