From f937b36efb442e5294a357c1a5a14e529aac3fa4 Mon Sep 17 00:00:00 2001 From: Biju Kunjummen Date: Fri, 19 May 2023 17:12:36 -0700 Subject: [PATCH] feat: allow Spanner and Datastore Transaction Manager in same project (#1412) Fixes #944 Conditional for Spanner is based on missing beans of type SpannerTransactionManager instead of PlatformTransactionManager, this way it always gets created if Spanner libs are pulled in Conditional for Datastore is based on missing beans of type DatastoreTransactionManager instead of PlatformTransactionManager, this way it gets created if Datastore libs are pulled in. It is the user's responsibility to designate the transaction's right transaction manager: ``` @Transactional(transactionManager = "spannerTransactionManager") ``` ``` @Transactional(transactionManager = "datastoreTransactionManager") ``` --- docs/src/main/asciidoc/datastore.adoc | 7 +++ docs/src/main/asciidoc/spanner.adoc | 8 ++++ docs/src/main/md/datastore.md | 9 ++++ docs/src/main/md/spanner.md | 9 ++++ ...reTransactionManagerAutoConfiguration.java | 3 +- ...erTransactionManagerAutoConfiguration.java | 3 +- .../README.adoc | 1 + .../example/MultipleDataModuleExample.java | 24 +++++------ .../main/java/com/example/PersonService.java | 43 +++++++++++++++++++ .../main/java/com/example/TraderService.java | 43 +++++++++++++++++++ 10 files changed, 134 insertions(+), 16 deletions(-) create mode 100644 spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/PersonService.java create mode 100644 spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/TraderService.java diff --git a/docs/src/main/asciidoc/datastore.adoc b/docs/src/main/asciidoc/datastore.adoc index 9d34d63328..018d744835 100644 --- a/docs/src/main/asciidoc/datastore.adoc +++ b/docs/src/main/asciidoc/datastore.adoc @@ -854,6 +854,13 @@ This feature requires a bean of `DatastoreTransactionManager`, which is provided If a method annotated with `@Transactional` calls another method also annotated, then both methods will work within the same transaction. `performTransaction` cannot be used in `@Transactional` annotated methods because Cloud Datastore does not support transactions within transactions. +Other Google Cloud database-related integrations like Spanner and Firestore can introduce `PlatformTransactionManager` beans, and can interfere with Datastore Transaction Manager. To disambiguate, explicitly specify the name of the transaction manager bean for such `@Transactional` methods. Example: + +[source,java] +---- +@Transactional(transactionManager = "datastoreTransactionManager") +---- + ==== Read-Write Support for Maps You can work with Maps of type `Map` instead of with entity objects by directly reading and writing them to and from Cloud Datastore. diff --git a/docs/src/main/asciidoc/spanner.adoc b/docs/src/main/asciidoc/spanner.adoc index 76330e057d..cf6b1f7228 100644 --- a/docs/src/main/asciidoc/spanner.adoc +++ b/docs/src/main/asciidoc/spanner.adoc @@ -956,6 +956,14 @@ This feature requires a bean of `SpannerTransactionManager`, which is provided w If a method annotated with `@Transactional` calls another method also annotated, then both methods will work within the same transaction. `performReadOnlyTransaction` and `performReadWriteTransaction` cannot be used in `@Transactional` annotated methods because Cloud Spanner does not support transactions within transactions. +Other Google Cloud database-related integrations like Spanner and Firestore can introduce `PlatformTransactionManager` beans, and can interfere with Datastore Transaction Manager. To disambiguate, explicitly specify the name of the transaction manager bean for such `@Transactional` methods. Example: + +[source,java] +---- +@Transactional(transactionManager = "spannerTransactionManager") +---- + + ==== DML Statements `SpannerTemplate` supports https://cloud.google.com/spanner/docs/dml-tasks:[DML] `Statements`. diff --git a/docs/src/main/md/datastore.md b/docs/src/main/md/datastore.md index e106a56499..04a8cc6fae 100644 --- a/docs/src/main/md/datastore.md +++ b/docs/src/main/md/datastore.md @@ -944,6 +944,15 @@ same transaction. `performTransaction` cannot be used in `@Transactional` annotated methods because Cloud Datastore does not support transactions within transactions. +Other Google Cloud database-related integrations like Spanner and Firestore can +introduce `PlatformTransactionManager` beans, and can interfere with Datastore +Transaction Manager. To disambiguate, explicitly specify the name of the +transaction manager bean for such `@Transactional` methods. Example: + +```java +@Transactional(transactionManager = "datastoreTransactionManager") +``` + #### Read-Write Support for Maps You can work with Maps of type `Map` instead of with entity diff --git a/docs/src/main/md/spanner.md b/docs/src/main/md/spanner.md index 54bee11e4e..b1d7e46b26 100644 --- a/docs/src/main/md/spanner.md +++ b/docs/src/main/md/spanner.md @@ -1123,6 +1123,15 @@ same transaction. `performReadOnlyTransaction` and annotated methods because Cloud Spanner does not support transactions within transactions. +Other Google Cloud database-related integrations like Spanner and Firestore can +introduce `PlatformTransactionManager` beans, and can interfere with Datastore +Transaction Manager. To disambiguate, explicitly specify the name of the +transaction manager bean for such `@Transactional` methods. Example: + +```java +@Transactional(transactionManager = "spannerTransactionManager") +``` + #### DML Statements `SpannerTemplate` supports diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/datastore/DatastoreTransactionManagerAutoConfiguration.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/datastore/DatastoreTransactionManagerAutoConfiguration.java index 83f990649a..3fc709b8d6 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/datastore/DatastoreTransactionManagerAutoConfiguration.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/datastore/DatastoreTransactionManagerAutoConfiguration.java @@ -26,7 +26,6 @@ import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; import org.springframework.context.annotation.Bean; -import org.springframework.transaction.PlatformTransactionManager; /** * Auto-configuration for {@link DatastoreTransactionManager}. @@ -59,7 +58,7 @@ static class DatastoreTransactionManagerConfiguration { } @Bean - @ConditionalOnMissingBean(PlatformTransactionManager.class) + @ConditionalOnMissingBean public DatastoreTransactionManager datastoreTransactionManager() { DatastoreTransactionManager transactionManager = new DatastoreTransactionManager(this.datastore); diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/spanner/SpannerTransactionManagerAutoConfiguration.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/spanner/SpannerTransactionManagerAutoConfiguration.java index 380fb419da..b6478fcbbe 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/spanner/SpannerTransactionManagerAutoConfiguration.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/spanner/SpannerTransactionManagerAutoConfiguration.java @@ -28,7 +28,6 @@ import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; import org.springframework.context.annotation.Bean; -import org.springframework.transaction.PlatformTransactionManager; /** * Auto-configuration for {@link SpannerTransactionManager}. @@ -61,7 +60,7 @@ static class DatabaseClientTransactionManagerConfiguration { } @Bean - @ConditionalOnMissingBean(PlatformTransactionManager.class) + @ConditionalOnMissingBean public SpannerTransactionManager spannerTransactionManager() { SpannerTransactionManager transactionManager = new SpannerTransactionManager(this.databaseClientProvider); diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/README.adoc b/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/README.adoc index 7cec965901..64cec327f5 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/README.adoc +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/README.adoc @@ -1,6 +1,7 @@ = Spring Framework on Google Cloud Cloud Spanner and Datastore Combined Usage Example This code sample demonstrates how to use both link:../../spring-cloud-gcp-starters/spring-cloud-gcp-starter-data-spanner[Spring Data Cloud Spanner] and link:../../spring-cloud-gcp-starters/spring-cloud-gcp-starter-data-datastore[Spring Data Cloud Datastore] using the Spring Data Cloud Datastore modules in a single Spring application. +This sample also demonstrates usage of transactions with both modules. == Running the example diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/MultipleDataModuleExample.java b/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/MultipleDataModuleExample.java index ad67396310..26da215523 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/MultipleDataModuleExample.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/MultipleDataModuleExample.java @@ -26,11 +26,11 @@ @SpringBootApplication public class MultipleDataModuleExample { - // A Spring Data Datastore repository - @Autowired PersonRepository personRepository; + // Internally uses a Spring Data Datastore repository + @Autowired private PersonService personService; - // A Spring Data Cloud Spanner repository - @Autowired TraderRepository traderRepository; + // Internally uses a Spring Data Cloud Spanner repository + @Autowired private TraderService traderService; public static void main(String[] args) { SpringApplication.run(MultipleDataModuleExample.class, args); @@ -41,19 +41,19 @@ ApplicationRunner applicationRunner() { return args -> { System.out.println("Deleting all entities."); - this.personRepository.deleteAll(); - this.traderRepository.deleteAll(); + this.personService.deleteAll(); + this.traderService.deleteAll(); - System.out.println("The number of Person entities is now: " + this.personRepository.count()); - System.out.println("The number of Trader entities is now: " + this.traderRepository.count()); + System.out.println("The number of Person entities is now: " + this.personService.count()); + System.out.println("The number of Trader entities is now: " + this.traderService.count()); System.out.println("Saving one entity with each repository."); - this.traderRepository.save(new Trader("id1", "trader", "one")); - this.personRepository.save(new Person(1L, "person1")); + this.traderService.save(new Trader("id1", "trader", "one")); + this.personService.save(new Person(1L, "person1")); - System.out.println("The number of Person entities is now: " + this.personRepository.count()); - System.out.println("The number of Trader entities is now: " + this.traderRepository.count()); + System.out.println("The number of Person entities is now: " + this.personService.count()); + System.out.println("The number of Trader entities is now: " + this.traderService.count()); }; } } diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/PersonService.java b/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/PersonService.java new file mode 100644 index 0000000000..067a969d13 --- /dev/null +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/PersonService.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Google LLC + * + * 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; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(transactionManager = "spannerTransactionManager") +public class PersonService { + + private final PersonRepository personRepository; + + public PersonService(PersonRepository personRepository) { + this.personRepository = personRepository; + } + + public Person save(Person person) { + return this.personRepository.save(person); + } + + public void deleteAll() { + this.personRepository.deleteAll(); + } + + public Long count() { + return this.personRepository.count(); + } +} diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/TraderService.java b/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/TraderService.java new file mode 100644 index 0000000000..9cdc35fd35 --- /dev/null +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-multi-sample/src/main/java/com/example/TraderService.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Google LLC + * + * 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; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(transactionManager = "datastoreTransactionManager") +public class TraderService { + + private final TraderRepository traderRepository; + + public TraderService(TraderRepository traderRepository) { + this.traderRepository = traderRepository; + } + + public Trader save(Trader trader) { + return this.traderRepository.save(trader); + } + + public void deleteAll() { + this.traderRepository.deleteAll(); + } + + public Long count() { + return this.traderRepository.count(); + } +}