Ignore non-default candidates in type-based matching

Closes gh-22403
This commit is contained in:
Andy Wilkinson
2024-08-20 11:40:04 +01:00
parent 8183c5b23d
commit 2ecb4ebd49
18 changed files with 271 additions and 179 deletions

View File

@@ -110,43 +110,41 @@ See xref:reference:data/sql.adoc#data.sql.datasource[] in the "`Spring Boot Feat
[[howto.data-access.configure-two-datasources]]
== Configure Two DataSources
If you need to configure multiple data sources, you can apply the same tricks that were described in the previous section.
You must, however, mark one of the `DataSource` instances as `@Primary`, because various auto-configurations down the road expect to be able to get one by type.
To define an additional `DataSource`, an approach that's similar to the previous section can be used.
A key difference is that the `DataSource` `@Bean` must be declared with `defaultCandidate=false`.
This prevents the auto-configured `DataSource` from backing off.
To allow the additional `DataSource` to be injected where it's needed, also annotate it with `@Qualifier` as shown in the following example:
If you create your own `DataSource`, the auto-configuration backs off.
In the following example, we provide the _exact_ same feature set as the auto-configuration provides on the primary data source:
include-code::MyAdditionalDataSourceConfiguration[]
include-code::MyDataSourcesConfiguration[]
To consume the additional `DataSource`, annotate the injection point with the same `@Qualifier`.
TIP: `firstDataSourceProperties` has to be flagged as `@Primary` so that the database initializer feature uses your copy (if you use the initializer).
Both data sources are also bound for advanced customizations.
For instance, you could configure them as follows:
The auto-configured and additional data sources can be configured as follows:
[configprops%novalidate,yaml]
----
spring:
datasource:
url: "jdbc:mysql://localhost/first"
username: "dbuser"
password: "dbpass"
configuration:
maximum-pool-size: 30
app:
datasource:
first:
url: "jdbc:mysql://localhost/first"
username: "dbuser"
password: "dbpass"
configuration:
maximum-pool-size: 30
second:
url: "jdbc:mysql://localhost/second"
username: "dbuser"
password: "dbpass"
max-total: 30
url: "jdbc:mysql://localhost/second"
username: "dbuser"
password: "dbpass"
max-total: 30
----
You can apply the same concept to the secondary `DataSource` as well, as shown in the following example:
More advanced, implementation-specific, configuration of the auto-configured `DataSource` is available through the `spring.datasource.configuration.*` properties.
You can apply the same concept to the additional `DataSource` as well, as shown in the following example:
include-code::MyCompleteDataSourcesConfiguration[]
include-code::MyCompleteAdditionalDataSourceConfiguration[]
The preceding example configures two data sources on custom namespaces with the same logic as Spring Boot would use in auto-configuration.
Note that each `configuration` sub namespace provides advanced settings based on the chosen implementation.
The preceding example configures the additional data source with the same logic as Spring Boot would use in auto-configuration.
Note that the `app.datasource.configuration.*` properties provide advanced settings based on the chosen implementation.
@@ -295,26 +293,28 @@ You can disable or tune this behavior by registering a `HibernatePropertiesCusto
To take full control of the configuration of the `EntityManagerFactory`, you need to add a `@Bean` named '`entityManagerFactory`'.
Spring Boot auto-configuration switches off its entity manager in the presence of a bean of that type.
NOTE: When you create a bean for `LocalContainerEntityManagerFactoryBean` yourself, any customization that was applied during the creation of the auto-configured `LocalContainerEntityManagerFactoryBean` is lost.
Make sure to use the auto-configured `EntityManagerFactoryBuilder` to retain JPA and vendor properties.
This is particularly important if you were relying on `spring.jpa.*` properties for configuring things like the naming strategy or the DDL mode.
[[howto.data-access.use-multiple-entity-managers]]
== Using Multiple EntityManagerFactories
If you need to use JPA against multiple data sources, you likely need one `EntityManagerFactory` per data source.
If you need to use JPA against multiple datasources, you likely need one `EntityManagerFactory` per datasource.
The `LocalContainerEntityManagerFactoryBean` from Spring ORM allows you to configure an `EntityManagerFactory` for your needs.
You can also reuse `JpaProperties` to bind settings for each `EntityManagerFactory`, as shown in the following example:
You can also reuse `JpaProperties` to bind settings for a second `EntityManagerFactory`.
Building upon xref:how-to:data-access.adoc#howto.data-access.configure-two-datasources[the example for configuring a second `DataSource`], a second `EntityManagerFactory` can be defined as shown in the following example:
include-code::MyEntityManagerFactoryConfiguration[]
include-code::MyAdditionalEntityManagerFactoryConfiguration[]
The example above creates an `EntityManagerFactory` using a `DataSource` bean named `firstDataSource`.
The example above creates an `EntityManagerFactory` using the `DataSource` bean qualified with `@Qualifier("second")`.
It scans entities located in the same package as `Order`.
It is possible to map additional JPA properties using the `app.first.jpa` namespace.
It is possible to map additional JPA properties using the `app.jpa` namespace.
The use of `@Bean(defaultCandidate=false)` allows the `secondJpaProperties` and `secondEntityManagerFactory` beans to be defined without interfering with auto-configured beans of the same type.
NOTE: When you create a bean for `LocalContainerEntityManagerFactoryBean` yourself, any customization that was applied during the creation of the auto-configured `LocalContainerEntityManagerFactoryBean` is lost.
Make sure to use the auto-configured `EntityManagerFactoryBuilder` to retain JPA and vendor properties.
This is particularly important if you were relying on `spring.jpa.*` properties for configuring things like the naming strategy or the DDL mode.
You should provide a similar configuration for any additional data sources for which you need JPA access.
You should provide a similar configuration for any more additional data sources for which you need JPA access.
To complete the picture, you need to configure a `JpaTransactionManager` for each `EntityManagerFactory` as well.
Alternatively, you might be able to use a JTA transaction manager that spans both.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 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.
@@ -16,35 +16,20 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration(proxyBeanMethods = false)
public class MyDataSourcesConfiguration {
public class MyAdditionalDataSourceConfiguration {
@Bean
@Primary
@ConfigurationProperties("app.datasource.first")
public DataSourceProperties firstDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("app.datasource.first.configuration")
public HikariDataSource firstDataSource(DataSourceProperties firstDataSourceProperties) {
return firstDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("app.datasource.second")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource")
public BasicDataSource secondDataSource() {
return DataSourceBuilder.create().type(BasicDataSource.class).build();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 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.
@@ -16,7 +16,6 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -24,33 +23,20 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration(proxyBeanMethods = false)
public class MyCompleteDataSourcesConfiguration {
public class MyCompleteAdditionalDataSourceConfiguration {
@Bean
@Primary
@ConfigurationProperties("app.datasource.first")
public DataSourceProperties firstDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("app.datasource.first.configuration")
public HikariDataSource firstDataSource(DataSourceProperties firstDataSourceProperties) {
return firstDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("app.datasource.second")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource")
public DataSourceProperties secondDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("app.datasource.second.configuration")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource.configuration")
public BasicDataSource secondDataSource(
@Qualifier("secondDataSourceProperties") DataSourceProperties secondDataSourceProperties) {
return secondDataSourceProperties.initializeDataSourceBuilder().type(BasicDataSource.class).build();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 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.
@@ -18,6 +18,7 @@ package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
@@ -28,19 +29,21 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
@Configuration(proxyBeanMethods = false)
public class MyEntityManagerFactoryConfiguration {
public class MyAdditionalEntityManagerFactoryConfiguration {
@Bean
@ConfigurationProperties("app.jpa.first")
public JpaProperties firstJpaProperties() {
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.jpa")
public JpaProperties secondJpaProperties() {
return new JpaProperties();
}
@Bean
public LocalContainerEntityManagerFactoryBean firstEntityManagerFactory(DataSource firstDataSource,
JpaProperties firstJpaProperties) {
EntityManagerFactoryBuilder builder = createEntityManagerFactoryBuilder(firstJpaProperties);
return builder.dataSource(firstDataSource).packages(Order.class).persistenceUnit("firstDs").build();
@Qualifier("second")
@Bean(defaultCandidate = false)
public LocalContainerEntityManagerFactoryBean secondEntityManagerFactory(@Qualifier("second") DataSource dataSource,
@Qualifier("second") JpaProperties jpaProperties) {
EntityManagerFactoryBuilder builder = createEntityManagerFactoryBuilder(jpaProperties);
return builder.dataSource(dataSource).packages(Order.class).persistenceUnit("second").build();
}
private EntityManagerFactoryBuilder createEntityManagerFactoryBuilder(JpaProperties jpaProperties) {

View File

@@ -16,6 +16,6 @@
package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers;
class Order {
public class Order {
}

View File

@@ -20,7 +20,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration(proxyBeanMethods = false)
@EnableJpaRepositories(basePackageClasses = Order.class, entityManagerFactoryRef = "firstEntityManagerFactory")
@EnableJpaRepositories(basePackageClasses = Order.class, entityManagerFactoryRef = "entityManagerFactory")
public class OrderConfiguration {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2024 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.
@@ -16,34 +16,20 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources
import com.zaxxer.hikari.HikariDataSource
import org.apache.commons.dbcp2.BasicDataSource
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.jdbc.DataSourceBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
@Configuration(proxyBeanMethods = false)
class MyDataSourcesConfiguration {
class MyAdditionalDataSourceConfiguration {
@Bean
@Primary
@ConfigurationProperties("app.datasource.first")
fun firstDataSourceProperties(): DataSourceProperties {
return DataSourceProperties()
}
@Bean
@Primary
@ConfigurationProperties("app.datasource.first.configuration")
fun firstDataSource(firstDataSourceProperties: DataSourceProperties): HikariDataSource {
return firstDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource::class.java).build()
}
@Bean
@ConfigurationProperties("app.datasource.second")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource")
fun secondDataSource(): BasicDataSource {
return DataSourceBuilder.create().type(BasicDataSource::class.java).build()
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2024 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.
@@ -16,39 +16,27 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources
import com.zaxxer.hikari.HikariDataSource
import org.apache.commons.dbcp2.BasicDataSource
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
@Configuration(proxyBeanMethods = false)
class MyCompleteDataSourcesConfiguration {
class MyCompleteAdditionalDataSourceConfiguration {
@Bean
@Primary
@ConfigurationProperties("app.datasource.first")
fun firstDataSourceProperties(): DataSourceProperties {
return DataSourceProperties()
}
@Bean
@Primary
@ConfigurationProperties("app.datasource.first.configuration")
fun firstDataSource(firstDataSourceProperties: DataSourceProperties): HikariDataSource {
return firstDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource::class.java).build()
}
@Bean
@ConfigurationProperties("app.datasource.second")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource")
fun secondDataSourceProperties(): DataSourceProperties {
return DataSourceProperties()
}
@Bean
@ConfigurationProperties("app.datasource.second.configuration")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource.configuration")
fun secondDataSource(secondDataSourceProperties: DataSourceProperties): BasicDataSource {
return secondDataSourceProperties.initializeDataSourceBuilder().type(BasicDataSource::class.java).build()
}

View File

@@ -18,6 +18,7 @@ package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers
import javax.sql.DataSource
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder
@@ -29,21 +30,23 @@ import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter
@Suppress("UNUSED_PARAMETER")
@Configuration(proxyBeanMethods = false)
class MyEntityManagerFactoryConfiguration {
class MyAdditionalEntityManagerFactoryConfiguration {
@Bean
@ConfigurationProperties("app.jpa.first")
fun firstJpaProperties(): JpaProperties {
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.jpa")
fun secondJpaProperties(): JpaProperties {
return JpaProperties()
}
@Bean
@Qualifier("second")
@Bean(defaultCandidate = false)
fun firstEntityManagerFactory(
firstDataSource: DataSource?,
firstJpaProperties: JpaProperties
@Qualifier("second") dataSource: DataSource,
@Qualifier("second") jpaProperties: JpaProperties
): LocalContainerEntityManagerFactoryBean {
val builder = createEntityManagerFactoryBuilder(firstJpaProperties)
return builder.dataSource(firstDataSource).packages(Order::class.java).persistenceUnit("firstDs").build()
val builder = createEntityManagerFactoryBuilder(jpaProperties)
return builder.dataSource(dataSource).packages(Order::class.java).persistenceUnit("second").build()
}
private fun createEntityManagerFactoryBuilder(jpaProperties: JpaProperties): EntityManagerFactoryBuilder {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 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.
@@ -21,37 +21,41 @@ import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MyCompleteDataSourcesConfiguration}.
* Tests for {@link MyCompleteAdditionalDataSourceConfiguration}.
*
* @author Stephane Nicoll
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Import(MyCompleteDataSourcesConfiguration.class)
@Import(MyCompleteAdditionalDataSourceConfiguration.class)
class MyCompleteDataSourcesConfigurationTests {
@Autowired
private ApplicationContext context;
@Autowired
private DataSource dataSource;
@Autowired
@Qualifier("second")
private DataSource secondDataSource;
@Test
void validateConfiguration() throws SQLException {
assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(2);
DataSource dataSource = this.context.getBean(DataSource.class);
assertThat(this.context.getBean("firstDataSource")).isSameAs(dataSource);
assertThat(dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
DataSource secondDataSource = this.context.getBean("secondDataSource", DataSource.class);
assertThat(secondDataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
assertThat(this.context.getBean("dataSource")).isSameAs(this.dataSource);
assertThat(this.dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
assertThat(this.context.getBean("secondDataSource")).isSameAs(this.secondDataSource);
assertThat(this.secondDataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
}
}

View File

@@ -22,39 +22,44 @@ import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MyDataSourcesConfiguration}.
* Tests for {@link MyAdditionalDataSourceConfiguration}.
*
* @author Stephane Nicoll
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = { "app.datasource.second.url=jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1",
"app.datasource.second.max-total=42" })
@Import(MyDataSourcesConfiguration.class)
@SpringBootTest(properties = { "app.datasource.url=jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1", "app.datasource.max-total=42" })
@Import(MyAdditionalDataSourceConfiguration.class)
class MyDataSourcesConfigurationTests {
@Autowired
private ApplicationContext context;
@Autowired
private DataSource dataSource;
@Autowired
@Qualifier("second")
private DataSource secondDataSource;
@Test
void validateConfiguration() throws SQLException {
assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(2);
DataSource dataSource = this.context.getBean(DataSource.class);
assertThat(this.context.getBean("firstDataSource")).isSameAs(dataSource);
assertThat(dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
BasicDataSource secondDataSource = this.context.getBean("secondDataSource", BasicDataSource.class);
assertThat(secondDataSource.getUrl()).isEqualTo("jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1");
assertThat(secondDataSource.getMaxTotal()).isEqualTo(42);
assertThat(this.context.getBean("dataSource")).isSameAs(this.dataSource);
assertThat(this.dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
assertThat(this.context.getBean("secondDataSource")).isSameAs(this.secondDataSource);
assertThat(this.secondDataSource).extracting((dataSource) -> ((BasicDataSource) dataSource).getUrl())
.isEqualTo("jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1");
assertThat(this.secondDataSource).extracting((dataSource) -> ((BasicDataSource) dataSource).getMaxTotal())
.isEqualTo(42);
}
}