From 244f971d1a8424ad931d72ef55e40f867b840b3f Mon Sep 17 00:00:00 2001 From: mikereiche Date: Tue, 9 May 2023 11:23:42 -0700 Subject: [PATCH] Add Couchbase Transactions Example. Closes #650. Original pull request: #663 --- couchbase/pom.xml | 1 + couchbase/transactions/README.md | 17 +++ couchbase/transactions/pom.xml | 80 ++++++++++++ .../java/com/example/demo/AirlineGates.java | 62 ++++++++++ .../example/demo/AirlineGatesRepository.java | 30 +++++ .../com/example/demo/AirlineGatesService.java | 103 ++++++++++++++++ .../main/java/com/example/demo/CmdRunner.java | 115 ++++++++++++++++++ .../main/java/com/example/demo/Config.java | 61 ++++++++++ .../com/example/demo/DemoApplication.java | 33 +++++ .../src/main/resources/application.properties | 1 + .../src/main/resources/logback.xml | 30 +++++ .../example/demo/DemoApplicationTests.java | 33 +++++ 12 files changed, 566 insertions(+) create mode 100644 couchbase/transactions/README.md create mode 100644 couchbase/transactions/pom.xml create mode 100644 couchbase/transactions/src/main/java/com/example/demo/AirlineGates.java create mode 100644 couchbase/transactions/src/main/java/com/example/demo/AirlineGatesRepository.java create mode 100644 couchbase/transactions/src/main/java/com/example/demo/AirlineGatesService.java create mode 100644 couchbase/transactions/src/main/java/com/example/demo/CmdRunner.java create mode 100644 couchbase/transactions/src/main/java/com/example/demo/Config.java create mode 100644 couchbase/transactions/src/main/java/com/example/demo/DemoApplication.java create mode 100644 couchbase/transactions/src/main/resources/application.properties create mode 100644 couchbase/transactions/src/main/resources/logback.xml create mode 100644 couchbase/transactions/src/test/java/com/example/demo/DemoApplicationTests.java 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() { + } +}