GoogleCloudPlatform / spring-cloud-gcp

New home for Spring Cloud GCP development starting with version 2.0.
Apache License 2.0
416 stars 307 forks source link

Transaction error using Spanner JDBC with JPA and Spring Cloud GCP Datastore in the same project #944

Closed lucasoares closed 1 year ago

lucasoares commented 2 years ago

Hello.

I'm trying to configure a single project using Spring Data JPA + Spanner using JDBC connector and also the Spring Cloud GCP Datastore support.

I'm able to read data from both Spanner and Datastore even tho they are located in different GCP projects. The problem is when I try to save any entity (or perform a primary key operation which have a transaction behind the scenes) I get the following error:

2022-02-10 21:49:27,317 ERROR  [page.PageSync] Error creating page of id 4324324234234 org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'transactionManager' available: No matching TransactionManager bean found for qualifier 'transactionManager' - neither qualifier match nor bean name match!
    at org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils.qualifiedBeanOfType(BeanFactoryAnnotationUtils.java:136) ~[spring-beans-5.3.8.jar:5.3.8]
    at org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils.qualifiedBeanOfType(BeanFactoryAnnotationUtils.java:95) ~[spring-beans-5.3.8.jar:5.3.8]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.determineQualifiedTransactionManager(TransactionAspectSupport.java:515) ~[spring-tx-5.3.8.jar:5.3.8]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.determineTransactionManager(TransactionAspectSupport.java:496) ~[spring-tx-5.3.8.jar:5.3.8]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:342) ~[spring-tx-5.3.8.jar:5.3.8]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.8.jar:5.3.8]

I'm basically reading data from Datastore and saving data into Spanner.

Code throwing error:

   this.pageRepository.save(page);

Repository implementation:

@Repository
public interface PageRepository extends JpaRepository<Page, UUID> {}

My project configuration (only what matters for this specific error):

  <properties>
    <!-- Spring -->
    <spring-cloud.version>2020.0.3</spring-cloud.version>
    <spring-cloud-gcp.version>2.0.3</spring-cloud-gcp.version>
    <spring.version>2.5.2</spring.version>
  </properties>

  <!-- I do not use any Spring Parent since I have my own parents -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring-cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>spring-cloud-gcp-dependencies</artifactId>
        <version>${spring-cloud-gcp.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>spring-cloud-gcp-starter-data-datastore</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-spanner-hibernate-dialect</artifactId>
      <version>1.5.2</version>
    </dependency>
  </dependencies>

WIth these properties:

spring.cloud.gcp.datastore.project-id=my-project-id
spring.datasource.url=jdbc:cloudspanner:/projects/other-project-id/instances/my-instance/databases/my-database
spring.datasource.driver-class-name=com.google.cloud.spanner.jdbc.JdbcDriver
spring.jpa.database-platform=com.google.cloud.spanner.hibernate.SpannerDialect

I can't use Spanner from Spring Cloud GCP because I already have a project using JPA and now I need to access datastore in the same project to copy data to Spanner.

If I remove the spring-cloud-gcp-starter-data-datastore dependency from the project (and comment all codes using datastore) I can save data into the Spanner instance without errors.

lucasoares commented 2 years ago

I tested with another application that doesn't use the Datastore itself but had the spring-cloud-gcp-data-datastore dependency in its classpath by mistake (was inheriting from another dependency) and the result is the same. Trying to insert data using JPA repository throws an exception.

For some reason it is not necessary to have the starter dependency spring-cloud-gcp-starter-data-datastore in the classpath to trigger this issue.

In this second application I just removed the spring-cloud-gcp-data-datastore from the classpath (using the exclusion above) and all inserts to Spanner using the JPA repository doesn't throw the exception anymore.

      <exclusions>
        <exclusion>
          <artifactId>spring-cloud-gcp-data-datastore</artifactId>
          <groupId>com.google.cloud</groupId>
        </exclusion>
      </exclusions>

It makes me think if the problem is with the datastore dependency itself or with the JPA starter auto configurations not being configured properly because of the datastore dependency.

elefeint commented 2 years ago

The second issue is because Spring Cloud GCP autoconfiguration for Datastore triggers as soon as the client library is detected.

Let me think through whether this conflict (Spanner with JPA, but Datastore with a custom Spring Data implementation) can be avoided in a sensible way. Are you using Datastore repositories, or just the template to read data?

In the meantime, turn off spring.cloud.gcp.datastore.enabled property to disable autoconfiguration for Datastore. You will likely want to create your own DatastoreTemplate object like the autoconfiguration nomally does.

elefeint commented 2 years ago

I did not look into it, and I am going on vacation; someone else will pick this up.

@lucasoares Did disabling datastore autoconfiguration mitigate the issue for you?

gomezAlvaro commented 2 years ago

This is also happening for me, using only JPA + Datastore. Disabling the autoconfiguration is a workaround I guess, but not that nice.

It started happening after upgrading the spring-cloud-gcp-data-datastore, from 1.2.8.RELEASE, where the error was not present, to 2.0.10 where it is. I also tried going to the last version 3.2.1, but the same error is still there.

lucasoares commented 2 years ago

I did not look into it, and I am going on vacation; someone else will pick this up.

@lucasoares Did disabling datastore autoconfiguration mitigate the issue for you?

I did not tried it, but in my use case this is not possible because I need the datastore. In the other project I had only the spring-cloud-gcp-data-datastore dependency, without the starter dependency and the same error occurs.

I don't think the problem is the autoconfiguration itself, since it comes from the starter dependency.

elefeint commented 2 years ago

@zhumin8 Could you reproduce this and reason about what's going on here?

jdfischer-cedreo commented 1 year ago

For those who have the issue with Jpa + datastore you can workaround the issue by declaring in your own configuration the two transaction manager.

@Configuration
public class MixingJpaAndDatastoreConfiguration
{

    // ============================================================ //
    // ========================== DATASTORE ======================= //
    // ============================================================ //

    @Bean
    @ConditionalOnMissingBean(name = "datastoreTransactionManager")
    public DatastoreTransactionManager datastoreTransactionManager(final DatastoreProvider datastore,
                                                                   final ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers)
    {
        final TransactionManagerCustomizers transactionManagerCustomizers1 = transactionManagerCustomizers.getIfAvailable();
        final DatastoreTransactionManager transactionManager = new DatastoreTransactionManager(datastore);
        if (transactionManagerCustomizers1 != null)
        {
            transactionManagerCustomizers1.customize(transactionManager);
        }

        return transactionManager;
    }

    // ============================================================ //
    // =========================== MYSQL ========================== //
    // ============================================================ //

    @Bean
    @ConditionalOnMissingBean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(final ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers)
    {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManagerCustomizers.ifAvailable((customizers) ->
                                                  {
                                                      customizers.customize(transactionManager);
                                                  });
        return transactionManager;
    }
}

You will need to specify on your @Transactional which manager you want but you will have both available. For example, jpa transaction will need @Transactional(transactionManager = "transactionManager").