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
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.