javaspringspring-boot

Spring boot with 2 DataSources


I am trying to set up a spring boot application using 2 datasources.

I found this repo: https://github.com/kota65535/spring-data-jdbc-multi-repos/tree/master

It is exactly what I need, 2 DS, no JPA. I cloned it and it works.

I set up my project with exactly the same configuration. And it was not working, when I start I get the error :

No qualifying bean of type 'org.springframework.data.jdbc.core.mapping.JdbcMappingContext' available: expected single matching bean but found 2

There is 2 JdbcMappingContext, 2 datasource, 2 transactionManager etc..

The only difference is that I am using Spring boot 3.3.4 vs 3.1.5 in the example. I switch my project to 3.1.5 and no more error it works fine.

How I can use 2 datasources with an up to date Spring (maybe a change in Spring 6.0 vs 6.1) ?

Thanks


Solution

  • TL;DR

    I made https://github.com/kota65535/spring-data-jdbc-multi-repos work with Spring Boot 3.4.1. Pull request here.

    All beans in Db1Config.java had to be annotated as primary. I also had to bump versions for a couple of dependencies.

    I updated the test just to make sure both data sources are still in use (see below).

    settings.gradle: Bumped versions

    pluginManagement {
        repositories {
            gradlePluginPortal()
            mavenLocal()
            maven {
                url "https://repo.spring.io/milestone"
            }
        }
        plugins {
            id "org.springframework.boot" version springBootVersion
            id "io.freefair.lombok" version "8.11"
            id "com.avast.gradle.docker-compose" version "0.17.12"
        }
    }
    
    rootProject.name = "spring-data-jdbc-test"
    

    Db1Config.java: Made all beans primary

    package com.kota65535.config;
    
    
    import com.kota65535.config.Db1Config.JdbcRepositoryFactoryBeanDb1;
    import com.zaxxer.hikari.HikariDataSource;
    import java.io.Serializable;
    import java.util.Optional;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
    import org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration;
    import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
    import org.springframework.boot.jdbc.DataSourceBuilder;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.context.annotation.Primary;
    import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
    import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
    import org.springframework.data.jdbc.core.convert.JdbcConverter;
    import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
    import org.springframework.data.jdbc.core.convert.RelationResolver;
    import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
    import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
    import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
    import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean;
    import org.springframework.data.relational.RelationalManagedTypes;
    import org.springframework.data.relational.core.dialect.Dialect;
    import org.springframework.data.relational.core.mapping.NamingStrategy;
    import org.springframework.data.relational.core.mapping.RelationalMappingContext;
    import org.springframework.data.repository.Repository;
    import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
    import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
    import org.springframework.jdbc.support.JdbcTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    
    @EnableJdbcRepositories(
        repositoryFactoryBeanClass = JdbcRepositoryFactoryBeanDb1.class,
        transactionManagerRef = "transactionManagerDb1",
        basePackages = {
            "com.kota65535.repository.one"
        }
    )
    @EnableAutoConfiguration(
        exclude = {DataSourceAutoConfiguration.class, JdbcRepositoriesAutoConfiguration.class}
    )
    @Configuration
    @ConfigurationPropertiesScan
    public class Db1Config {
    
      private final AbstractJdbcConfiguration base;
    
      public Db1Config(ApplicationContext applicationContext) {
        this.base = new AbstractJdbcConfiguration();
        this.base.setApplicationContext(applicationContext);
      }
    
      @Primary
      @Bean
      @Qualifier("db1")
      @ConfigurationProperties(prefix = "spring.datasources.one")
      public HikariDataSource dataSourceDb1() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
      }
    
      @Primary
      @Bean
      @Qualifier("db1")
      public NamedParameterJdbcOperations jdbcOperationsDb1(
          @Qualifier("db1") DataSource dataSource
      ) {
        return new NamedParameterJdbcTemplate(dataSource);
      }
    
      @Primary
      @Bean
      @Qualifier("db1")
      public PlatformTransactionManager transactionManagerDb1(
          @Qualifier("db1") DataSource dataSource
      ) {
        return new JdbcTransactionManager(dataSource);
      }
    
      @Primary
      @Bean
      @Qualifier("db1")
      public RelationalManagedTypes jdbcManagedTypesDb1() throws ClassNotFoundException {
        return base.jdbcManagedTypes();
      }
    
      @Primary
      @Bean
      @Qualifier("db1")
      public JdbcMappingContext jdbcMappingContextDb1(
          Optional<NamingStrategy> namingStrategy,
          @Qualifier("db1") JdbcCustomConversions customConversions,
          @Qualifier("db1") RelationalManagedTypes jdbcManagedTypes) {
        return base.jdbcMappingContext(namingStrategy, customConversions, jdbcManagedTypes);
      }
    
      @Primary
      @Bean
      @Qualifier("db1")
      public JdbcConverter jdbcConverterDb1(
          @Qualifier("db1") JdbcMappingContext mappingContext,
          @Qualifier("db1") NamedParameterJdbcOperations operations,
          @Qualifier("db1") @Lazy RelationResolver relationResolver,
          @Qualifier("db1") JdbcCustomConversions conversions,
          @Qualifier("db1") Dialect dialect) {
        return base.jdbcConverter(mappingContext, operations, relationResolver, conversions, dialect);
      }
    
      @Primary
      @Bean
      @Qualifier("db1")
      public JdbcCustomConversions jdbcCustomConversionsDb1() {
        return base.jdbcCustomConversions();
      }
    
      @Primary
      @Bean
      @Qualifier("db1")
      public JdbcAggregateTemplate jdbcAggregateTemplateDb1(
          ApplicationContext applicationContext,
          @Qualifier("db1") JdbcMappingContext mappingContext,
          @Qualifier("db1") JdbcConverter converter,
          @Qualifier("db1") DataAccessStrategy dataAccessStrategy) {
        return base.jdbcAggregateTemplate(applicationContext, mappingContext, converter, dataAccessStrategy);
      }
    
      @Primary
      @Bean
      @Qualifier("db1")
      public DataAccessStrategy dataAccessStrategyDb1(
          @Qualifier("db1") NamedParameterJdbcOperations operations,
          @Qualifier("db1") JdbcConverter jdbcConverter,
          @Qualifier("db1") JdbcMappingContext context,
          @Qualifier("db1") Dialect dialect) {
        return base.dataAccessStrategyBean(operations, jdbcConverter, context, dialect);
      }
    
      @Primary
      @Bean
      @Qualifier("db1")
      public Dialect jdbcDialectDb1(@Qualifier("db1") NamedParameterJdbcOperations operations) {
        return base.jdbcDialect(operations);
      }
    
      public static class JdbcRepositoryFactoryBeanDb1<T extends Repository<S, ID>, S, ID extends Serializable> extends
          JdbcRepositoryFactoryBean<T, S, ID> {
    
        public JdbcRepositoryFactoryBeanDb1(Class<T> repositoryInterface) {
          super(repositoryInterface);
        }
    
        @Override
        @Autowired
        public void setDataAccessStrategy(@Qualifier("db1") DataAccessStrategy dataAccessStrategy) {
          super.setDataAccessStrategy(dataAccessStrategy);
        }
    
        @Override
        @Autowired
        public void setJdbcOperations(@Qualifier("db1") NamedParameterJdbcOperations operations) {
          super.setJdbcOperations(operations);
        }
    
        @Override
        @Autowired
        public void setMappingContext(@Qualifier("db1") RelationalMappingContext mappingContext) {
          super.setMappingContext(mappingContext);
        }
    
        @Override
        @Autowired
        public void setDialect(@Qualifier("db1") Dialect dialect) {
          super.setDialect(dialect);
        }
    
        @Override
        @Autowired
        public void setConverter(@Qualifier("db1") JdbcConverter converter) {
          super.setConverter(converter);
        }
      }
    }
    

    Updated test

    package com.kota65535;
    
    import static org.assertj.core.api.Assertions.assertThat;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    import static org.mockito.Mockito.verify;
    import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
    
    import com.kota65535.controller.User;
    import com.kota65535.controller.Users;
    import com.kota65535.repository.one.Db1UserRepository;
    import com.kota65535.repository.two.Db2UserRepository;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.web.server.LocalServerPort;
    import org.springframework.boot.web.client.RestTemplateBuilder;
    import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
    import org.springframework.web.client.RestTemplate;
    
    @SpringBootTest(webEnvironment = RANDOM_PORT)
    class UsersControllerTest {
    
      @LocalServerPort
      int port;
      RestTemplate client;
    
      @MockitoSpyBean
      Db1UserRepository repository1;
    
      @MockitoSpyBean
      Db2UserRepository repository2;
    
      @BeforeEach
      void beforeEach() {
        client = new RestTemplateBuilder()
            .rootUri("http://localhost:%d".formatted(port))
            .build();
      }
    
      @Test
      void testGetUsers() {
        Users users = client.getForObject("/users", Users.class);
        assertNotNull(users);
        assertThat(users.getUsers()).hasSize(4);
    
        // "foo", "bar" are from db1, "hoge", "piyo" from db2
        assertThat(users.getUsers())
                .extracting(User::getName)
                .containsExactlyInAnyOrder("foo", "bar", "hoge", "piyo");
    
        verify(repository1).findAll();
        verify(repository2).findAll();
      }
    }
    

    I also bumped gradlew to 8.12 and Spring Boot to 3.4.1.