環境版本:
- SpringBoot 2.1.3
JPA 官方文件
5.1.2. Annotation-based Configuration
The Spring Data JPA repositories support can be activated not only through an XML namespace but also by using an annotation through JavaConfig, as shown in the following example:
Example 51. Spring Data JPA repositories using JavaConfig
@Configuration @EnableJpaRepositories @EnableTransactionManagement class ApplicationConfig { @Bean public DataSource dataSource() { EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); return builder.setType(EmbeddedDatabaseType.HSQL).build(); } @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); vendorAdapter.setGenerateDdl(true); LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); factory.setJpaVendorAdapter(vendorAdapter); factory.setPackagesToScan("com.acme.domain"); factory.setDataSource(dataSource()); return factory; } @Bean public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager txManager = new JpaTransactionManager(); txManager.setEntityManagerFactory(entityManagerFactory); return txManager; } } 複製程式碼
文件中說明,基於註解的配置包含三個關鍵Bean:
PlatformTransactionManager
——事務管理LocalContainerEntityManagerFactoryBean
——實體類管理DataSource
——資料來源
同時還有一個關鍵註解@EnableJpaRepositories
SpringBoot官方文件
多資料來源使用@Primary
指明優先順序
You can even go further by leveraging what
DataSourceProperties
does for you — that is, by providing a default embedded database with a sensible username and password if no URL is provided. You can easily initialize aDataSourceBuilder
from the state of anyDataSourceProperties
object, so you could also inject the DataSource that Spring Boot creates automatically. However, that would split your configuration into two namespaces:url
,username
,password
,type
, anddriver
onspring.datasource
and the rest on your custom namespace (app.datasource
). To avoid that, you can redefine a customDataSourceProperties
on your custom namespace, as shown in the following example:
@Bean @Primary @ConfigurationProperties("app.datasource") public DataSourceProperties dataSourceProperties() { return new DataSourceProperties(); } @Bean @ConfigurationProperties("app.datasource.configuration") public HikariDataSource dataSource(DataSourceProperties properties) { return properties.initializeDataSourceBuilder().type(HikariDataSource.class) .build(); } 複製程式碼
84.2 Configure Two DataSources
If you need to configure multiple data sources, you can apply the same tricks that are 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.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:
@Bean @Primary @ConfigurationProperties("app.datasource.first") public DataSourceProperties firstDataSourceProperties() { return new DataSourceProperties(); } @Bean @Primary @ConfigurationProperties("app.datasource.first.configuration") public HikariDataSource firstDataSource() { return firstDataSourceProperties().initializeDataSourceBuilder() .type(HikariDataSource.class).build(); } @Bean @ConfigurationProperties("app.datasource.second") public BasicDataSource secondDataSource() { return DataSourceBuilder.create().type(BasicDataSource.class).build(); } 複製程式碼
或者使用兩個EntityManagers
,並結合@EnableJpaRepositories
進行配置
84.10 Use Two EntityManagers
Even if the default
EntityManagerFactory
works fine, you need to define a new one. Otherwise, the presence of the second bean of that type switches off the default. To make it easy to do, you can use the convenientEntityManagerBuilder
provided by Spring Boot. Alternatively, you can just theLocalContainerEntityManagerFactoryBean
directly from Spring ORM, as shown in the following example:
// add two data sources configured as above @Bean public LocalContainerEntityManagerFactoryBean customerEntityManagerFactory( EntityManagerFactoryBuilder builder) { return builder .dataSource(customerDataSource()) .packages(Customer.class) .persistenceUnit("customers") .build(); } @Bean public LocalContainerEntityManagerFactoryBean orderEntityManagerFactory( EntityManagerFactoryBuilder builder) { return builder .dataSource(orderDataSource()) .packages(Order.class) .persistenceUnit("orders") .build(); } 複製程式碼
The configuration above almost works on its own. To complete the picture, you need to configure
TransactionManagers
for the twoEntityManagers
as well. If you mark one of them as@Primary
, it could be picked up by the defaultJpaTransactionManager
in Spring Boot. The other would have to be explicitly injected into a new instance. Alternatively, you might be able to use a JTA transaction manager that spans both.If you use Spring Data, you need to configure
@EnableJpaRepositories
accordingly, as shown in the following example:
@Configuration @EnableJpaRepositories(basePackageClasses = Customer.class, entityManagerFactoryRef = "customerEntityManagerFactory") public class CustomerConfiguration { ... } @Configuration @EnableJpaRepositories(basePackageClasses = Order.class, entityManagerFactoryRef = "orderEntityManagerFactory") public class OrderConfiguration { ... } 複製程式碼
原始碼分析
自動裝配類中宣告瞭配置Bean和條件裝配順序
package org.springframework.boot.autoconfigure.data.jpa;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Data's JPA Repositories.
* <p>
* Activates when there is a bean of type {@link javax.sql.DataSource} configured in the
* context, the Spring Data JPA
* {@link org.springframework.data.jpa.repository.JpaRepository} type is on the classpath,
* and there is no other, existing
* {@link org.springframework.data.jpa.repository.JpaRepository} configured.
* <p>
* Once in effect, the auto-configuration is the equivalent of enabling JPA repositories
* using the {@link org.springframework.data.jpa.repository.config.EnableJpaRepositories}
* annotation.
* <p>
* This configuration class will activate <em>after</em> the Hibernate auto-configuration.
*
* @author Phillip Webb
* @author Josh Long
* @see EnableJpaRepositories
*/
@Configuration
@ConditionalOnBean(DataSource.class)
@ConditionalOnClass(JpaRepository.class)
@ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class,
JpaRepositoryConfigExtension.class })
@ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "enabled", havingValue = "true", matchIfMissing = true)
@Import(JpaRepositoriesAutoConfigureRegistrar.class)
@AutoConfigureAfter({ HibernateJpaAutoConfiguration.class,
TaskExecutionAutoConfiguration.class })
public class JpaRepositoriesAutoConfiguration {
@Bean
@Conditional(BootstrapExecutorCondition.class)
public EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer(
Map<String, AsyncTaskExecutor> taskExecutors) {
return (builder) -> {
AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(
taskExecutors);
if (bootstrapExecutor != null) {
builder.setBootstrapExecutor(bootstrapExecutor);
}
};
}
private AsyncTaskExecutor determineBootstrapExecutor(
Map<String, AsyncTaskExecutor> taskExecutors) {
if (taskExecutors.size() == 1) {
return taskExecutors.values().iterator().next();
}
return taskExecutors
.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME);
}
private static final class BootstrapExecutorCondition extends AnyNestedCondition {
BootstrapExecutorCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "bootstrap-mode", havingValue = "deferred", matchIfMissing = false)
static class DeferredBootstrapMode {
}
@ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "bootstrap-mode", havingValue = "lazy", matchIfMissing = false)
static class LazyBootstrapMode {
}
}
}
複製程式碼
引入的配置類中宣告瞭EnableJpaRepositories
註解
package org.springframework.boot.autoconfigure.data.jpa;
class JpaRepositoriesAutoConfigureRegistrar
extends AbstractRepositoryConfigurationSourceSupport {
private BootstrapMode bootstrapMode = null;
@Override
protected Class<? extends Annotation> getAnnotation() {
return EnableJpaRepositories.class;
}
@Override
protected Class<?> getConfiguration() {
return EnableJpaRepositoriesConfiguration.class;
}
@Override
protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() {
return new JpaRepositoryConfigExtension();
}
@Override
protected BootstrapMode getBootstrapMode() {
return (this.bootstrapMode == null) ? super.getBootstrapMode()
: this.bootstrapMode;
}
@Override
public void setEnvironment(Environment environment) {
super.setEnvironment(environment);
configureBootstrapMode(environment);
}
private void configureBootstrapMode(Environment environment) {
String property = environment
.getProperty("spring.data.jpa.repositories.bootstrap-mode");
if (StringUtils.hasText(property)) {
this.bootstrapMode = BootstrapMode
.valueOf(property.toUpperCase(Locale.ENGLISH));
}
}
@EnableJpaRepositories
private static class EnableJpaRepositoriesConfiguration {}
}
複製程式碼
EnableJpaRepositories
註解定義
package org.springframework.data.jpa.repository.config;
/**
* Annotation to enable JPA repositories. Will scan the package of the annotated configuration class for Spring Data
* repositories by default.
*
* @author Oliver Gierke
* @author Thomas Darimont
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(JpaRepositoriesRegistrar.class)
public @interface EnableJpaRepositories {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Filter[] includeFilters() default {};
Filter[] excludeFilters() default {};
String repositoryImplementationPostfix() default "Impl";
String namedQueriesLocation() default "";
Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
Class<?> repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class;
Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
/**
* 實體管理工廠
* @return
*/
String entityManagerFactoryRef() default "entityManagerFactory";
/**
* 事務管理
* @return
*/
String transactionManagerRef() default "transactionManager";
boolean considerNestedRepositories() default false;
boolean enableDefaultTransactions() default true;
/**
* Configures when the repositories are initialized in the bootstrap lifecycle. {@link BootstrapMode#DEFAULT}
* (default) means eager initialization except all repository interfaces annotated with {@link Lazy},
* {@link BootstrapMode#LAZY} means lazy by default including injection of lazy-initialization proxies into client
* beans so that those can be instantiated but will only trigger the initialization upon first repository usage (i.e a
* method invocation on it). This means repositories can still be uninitialized when the application context has
* completed its bootstrap. {@link BootstrapMode#DEFERRED} is fundamentally the same as {@link BootstrapMode#LAZY},
* but triggers repository initialization when the application context finishes its bootstrap.
*
* @return
* @since 2.1
*/
BootstrapMode bootstrapMode() default BootstrapMode.DEFAULT;
}
複製程式碼
查詢關鍵Bean的註冊時機
package org.springframework.boot.autoconfigure.orm.jpa;
@Configuration
@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class })
@Conditional(HibernateEntityManagerCondition.class)
@EnableConfigurationProperties(JpaProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class })
@Import(HibernateJpaConfiguration.class)
public class HibernateJpaAutoConfiguration {}
複製程式碼
進入配置Bean
package org.springframework.boot.autoconfigure.orm.jpa;
@Configuration
@EnableConfigurationProperties(HibernateProperties.class)
@ConditionalOnSingleCandidate(DataSource.class)
class HibernateJpaConfiguration extends JpaBaseConfiguration {}
複製程式碼
在其父類JpaBaseConfiguration
中宣告瞭裝配時機和條件
package org.springframework.boot.autoconfigure.orm.jpa;
/**
* 該類在2.0版本進行了重構
* @since 2.0.0
*/
@Configuration
@EnableConfigurationProperties(JpaProperties.class)
@Import(DataSourceInitializedPublisher.Registrar.class)
public abstract class JpaBaseConfiguration implements BeanFactoryAware {
@Bean
@ConditionalOnMissingBean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
if (this.transactionManagerCustomizers != null) {
// 預設使用方法名稱
this.transactionManagerCustomizers.customize(transactionManager);
}
return transactionManager;
}
@Bean
@ConditionalOnMissingBean
public JpaVendorAdapter jpaVendorAdapter() {
AbstractJpaVendorAdapter adapter = createJpaVendorAdapter();
adapter.setShowSql(this.properties.isShowSql());
adapter.setDatabase(this.properties.determineDatabase(this.dataSource));
adapter.setDatabasePlatform(this.properties.getDatabasePlatform());
adapter.setGenerateDdl(this.properties.isGenerateDdl());
return adapter;
}
@Bean
@ConditionalOnMissingBean
public EntityManagerFactoryBuilder entityManagerFactoryBuilder(
JpaVendorAdapter jpaVendorAdapter,
ObjectProvider<PersistenceUnitManager> persistenceUnitManager,
ObjectProvider<EntityManagerFactoryBuilderCustomizer> customizers) {
EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(
jpaVendorAdapter, this.properties.getProperties(),
persistenceUnitManager.getIfAvailable());
customizers.orderedStream()
.forEach((customizer) -> customizer.customize(builder));
return builder;
}
@Bean
@Primary
@ConditionalOnMissingBean({ LocalContainerEntityManagerFactoryBean.class,
EntityManagerFactory.class })
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder factoryBuilder) {
Map<String, Object> vendorProperties = getVendorProperties();
customizeVendorProperties(vendorProperties);
return factoryBuilder.dataSource(this.dataSource).packages(getPackagesToScan())
.properties(vendorProperties).mappingResources(getMappingResources())
.jta(isJta()).build();
}
}
複製程式碼
測試
爬完文件和原始碼,對需要配置Bean有了基本瞭解,新建工程進行多資料來源測試
以當前最新版本SpringBoot進行測試
maven
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>top.fjy8018</groupId>
<artifactId>jpadatasource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jpadatasource</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
複製程式碼
實體類
通過包路徑區分主庫和備份庫
主庫實體類
package top.fjy8018.jpadatasource.entity.primary;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
/**
* @author F嘉陽
* @date 2019-03-30 9:11
*/
@Data
@Entity
public class Product {
@Id
private Integer id;
private String name;
private Integer price;
}
複製程式碼
備份庫實體類
package top.fjy8018.jpadatasource.entity.backup;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
/**
* @author F嘉陽
* @date 2019-03-30 9:36
*/
@Data
@Entity(name = "tb_order")
public class Order {
@Id
private Integer id;
private String orderName;
private Integer price;
}
複製程式碼
坑一:表名關鍵字衝突
若此處不指定表名為tb_order
則會與MySQL關鍵字衝突導致建表失敗
報錯:
org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "
create table Order (
id integer not null,
orderName varchar(255),
price integer,
primary key (id)
) engine=InnoDB" via JDBC Statement
...
複製程式碼
DAO
package top.fjy8018.jpadatasource.repository.backup;
import org.springframework.data.jpa.repository.JpaRepository;
import top.fjy8018.jpadatasource.entity.backup.Order;
/**
* @author F嘉陽
* @date 2019-03-30 9:12
*/
public interface OrderRepository extends JpaRepository<Order, Integer> {
}
複製程式碼
package top.fjy8018.jpadatasource.repository.primary;
import org.springframework.data.jpa.repository.JpaRepository;
import top.fjy8018.jpadatasource.entity.primary.Product;
/**
* @author F嘉陽
* @date 2019-03-30 9:12
*/
public interface ProductRepository extends JpaRepository<Product, Integer> {
}
複製程式碼
配置Bean
官方配置樣例:
Both data sources are also bound for advanced customizations. For instance, you could configure them as follows:
app.datasource.first.url=jdbc:mysql://localhost/first app.datasource.first.username=dbuser app.datasource.first.password=dbpass app.datasource.first.configuration.maximum-pool-size=30 app.datasource.second.url=jdbc:mysql://localhost/second app.datasource.second.username=dbuser app.datasource.second.password=dbpass app.datasource.second.max-total=30 複製程式碼
You can apply the same concept to the secondary
DataSource
as well, as shown in the following example:
@Bean @Primary @ConfigurationProperties("app.datasource.first") public DataSourceProperties firstDataSourceProperties() { return new DataSourceProperties(); } @Bean @Primary @ConfigurationProperties("app.datasource.first.configuration") public HikariDataSource firstDataSource() { return firstDataSourceProperties().initializeDataSourceBuilder() .type(HikariDataSource.class).build(); } @Bean @ConfigurationProperties("app.datasource.second") public DataSourceProperties secondDataSourceProperties() { return new DataSourceProperties(); } @Bean @ConfigurationProperties("app.datasource.second.configuration") public BasicDataSource secondDataSource() { return secondDataSourceProperties().initializeDataSourceBuilder() .type(BasicDataSource.class).build(); } 複製程式碼
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.
參照編寫配置檔案,此處注意SpringBoot 2.x的驅動變化
坑二:mysql驅動類名變化
spring:
datasource:
first:
driver-class-name: com.mysql.cj.jdbc.Driver
username: financial_adminer
password: financial_adminer_pass
url: jdbc:mysql://localhost/jpa_test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
configuration:
maximum-pool-size: 30
second:
driver-class-name: com.mysql.cj.jdbc.Driver
username: financial_adminer
password: financial_adminer_pass
url: jdbc:mysql://localhost/jpa_test_bak?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
configuration:
maximum-pool-size: 20
jpa:
hibernate:
ddl-auto: update
show-sql: true
database-platform: org.hibernate.dialect.MySQL55Dialect
properties:
hibernate:
format_sql: true
use_sql_comments: true
複製程式碼
配置類
package top.fjy8018.jpadatasource.config;
import com.zaxxer.hikari.HikariDataSource;
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;
/**
* @author F嘉陽
* @date 2019-03-30 9:24
*/
@Configuration
public class DataAccessConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.first")
public DataSourceProperties firstDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("spring.datasource.first.configuration")
public HikariDataSource firstDataSource() {
return firstDataSourceProperties().initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("spring.datasource.second")
public DataSourceProperties secondDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("spring.datasource.second.configuration")
public HikariDataSource secondDataSource() {
return secondDataSourceProperties().initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}
}
複製程式碼
配置完成後發現只在主庫自動建表,備份庫無變化,屬於預期情況,因為目前僅配置了三個必須Bean的其中一個
繼續配置實體管理和事務管理Bean
官方文件樣例
84.10 Use Two EntityManagers
Even if the default
EntityManagerFactory
works fine, you need to define a new one. Otherwise, the presence of the second bean of that type switches off the default. To make it easy to do, you can use the convenientEntityManagerBuilder
provided by Spring Boot. Alternatively, you can just theLocalContainerEntityManagerFactoryBean
directly from Spring ORM, as shown in the following example:
// add two data sources configured as above @Bean public LocalContainerEntityManagerFactoryBean customerEntityManagerFactory( EntityManagerFactoryBuilder builder) { return builder .dataSource(customerDataSource()) .packages(Customer.class) .persistenceUnit("customers") .build(); } @Bean public LocalContainerEntityManagerFactoryBean orderEntityManagerFactory( EntityManagerFactoryBuilder builder) { return builder .dataSource(orderDataSource()) .packages(Order.class) .persistenceUnit("orders") .build(); } 複製程式碼
The configuration above almost works on its own. To complete the picture, you need to configure
TransactionManagers
for the twoEntityManagers
as well. If you mark one of them as@Primary
, it could be picked up by the defaultJpaTransactionManager
in Spring Boot. The other would have to be explicitly injected into a new instance. Alternatively, you might be able to use a JTA transaction manager that spans both.If you use Spring Data, you need to configure
@EnableJpaRepositories
accordingly, as shown in the following example:
@Configuration @EnableJpaRepositories(basePackageClasses = Customer.class, entityManagerFactoryRef = "customerEntityManagerFactory") public class CustomerConfiguration { ... } @Configuration @EnableJpaRepositories(basePackageClasses = Order.class, entityManagerFactoryRef = "orderEntityManagerFactory") public class OrderConfiguration { ... } 複製程式碼
工程配置
package top.fjy8018.jpadatasource.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import top.fjy8018.jpadatasource.entity.backup.Order;
import top.fjy8018.jpadatasource.entity.primary.Product;
import top.fjy8018.jpadatasource.repository.backup.OrderRepository;
import top.fjy8018.jpadatasource.repository.primary.ProductRepository;
import javax.sql.DataSource;
/**
* @author F嘉陽
* @date 2019-03-30 9:24
*/
@Configuration
public class DataAccessConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.first")
public DataSourceProperties firstDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("spring.datasource.first.configuration")
public HikariDataSource firstDataSource() {
return firstDataSourceProperties().initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("spring.datasource.second")
public DataSourceProperties secondDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("spring.datasource.second.configuration")
public HikariDataSource secondDataSource() {
return secondDataSourceProperties().initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}
@Bean
public LocalContainerEntityManagerFactoryBean firstEntityManagerFactory(
EntityManagerFactoryBuilder builder, @Qualifier("firstDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages(Product.class)
.persistenceUnit("first")
.build();
}
@Bean
public LocalContainerEntityManagerFactoryBean secondEntityManagerFactory(
EntityManagerFactoryBuilder builder, @Qualifier("secondDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages(Order.class)
.persistenceUnit("second")
.build();
}
@Bean
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("firstEntityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory.getObject());
}
@Bean
public PlatformTransactionManager backupTransactionManager(
@Qualifier("secondEntityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory.getObject());
}
@EnableJpaRepositories(basePackageClasses = ProductRepository.class,
entityManagerFactoryRef = "firstEntityManagerFactory")
@Primary
public class PrimaryConfiguration {
}
@EnableJpaRepositories(basePackageClasses = OrderRepository.class,
entityManagerFactoryRef = "secondEntityManagerFactory")
public class secondConfiguration {
}
}
複製程式碼
坑三:官方文件樣例有誤
此時配置直接啟動會報錯
Method requestMappingHandlerMapping in org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration required a single bean, but 2 were found:
- firstEntityManagerFactory: defined by method 'firstEntityManagerFactory' in class path resource [top/fjy8018/jpadatasource/config/DataAccessConfig.class]
- secondEntityManagerFactory: defined by method 'secondEntityManagerFactory' in class path resource [top/fjy8018/jpadatasource/config/DataAccessConfig.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
複製程式碼
意思是自動裝配時發現兩個型別一致的Bean,主要指定優先順序,而官方文件對此處可能出現的問題並未提示
指定firstEntityManagerFactory
為優先Bean,同時文件樣例建立了事務管理器但未配置到@EnableJpaRepositories
註解中,此處也一併配置
最終配置類如下
package top.fjy8018.jpadatasource.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import top.fjy8018.jpadatasource.entity.backup.Order;
import top.fjy8018.jpadatasource.entity.primary.Product;
import top.fjy8018.jpadatasource.repository.backup.OrderRepository;
import top.fjy8018.jpadatasource.repository.primary.ProductRepository;
import javax.sql.DataSource;
/**
* @author F嘉陽
* @date 2019-03-30 9:24
*/
@Configuration
public class DataAccessConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.first")
public DataSourceProperties firstDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("spring.datasource.first.configuration")
public HikariDataSource firstDataSource() {
return firstDataSourceProperties().initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("spring.datasource.second")
public DataSourceProperties secondDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("spring.datasource.second.configuration")
public HikariDataSource secondDataSource() {
return secondDataSourceProperties().initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}
@Bean
public LocalContainerEntityManagerFactoryBean firstEntityManagerFactory(
EntityManagerFactoryBuilder builder, @Qualifier("firstDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages(Product.class)
.persistenceUnit("first")
.build();
}
@Bean
public LocalContainerEntityManagerFactoryBean secondEntityManagerFactory(
EntityManagerFactoryBuilder builder, @Qualifier("secondDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages(Order.class)
.persistenceUnit("second")
.build();
}
@Bean
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("firstEntityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory.getObject());
}
@Bean
public PlatformTransactionManager backupTransactionManager(
@Qualifier("secondEntityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory.getObject());
}
@EnableJpaRepositories(basePackageClasses = ProductRepository.class,
entityManagerFactoryRef = "firstEntityManagerFactory", transactionManagerRef = "primaryTransactionManager")
@Primary
public class PrimaryConfiguration {
}
@EnableJpaRepositories(basePackageClasses = OrderRepository.class,
entityManagerFactoryRef = "secondEntityManagerFactory", transactionManagerRef = "backupTransactionManager")
public class secondConfiguration {
}
}
複製程式碼
坑四:JPA不會自動建表
配置完成後啟動
發現日誌中建表SQL沒有輸出,同時檢視資料庫,確認表未建立
但官方文件對多資料來源的配置說明已經結束,令人疑惑
自動建表語句沒生成猜想可能由於與ddl相關的屬性未配置導致
進入配置檔案檢視ddl配置,發現預設為false
將其配置為true後總配置檔案如下
spring:
datasource:
first:
driver-class-name: com.mysql.cj.jdbc.Driver
username: financial_adminer
password: financial_adminer_pass
url: jdbc:mysql://120.79.226.26/jpa_test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
configuration:
maximum-pool-size: 30
second:
driver-class-name: com.mysql.cj.jdbc.Driver
username: financial_adminer
password: financial_adminer_pass
url: jdbc:mysql://120.79.226.26/jpa_test_bak?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
configuration:
maximum-pool-size: 20
jpa:
hibernate:
ddl-auto: update
show-sql: true
database-platform: org.hibernate.dialect.MySQL55Dialect
properties:
hibernate:
format_sql: true
use_sql_comments: true
generate-ddl: true
複製程式碼
啟動工程發現自動建表生效
檢視資料庫情況,與預期一致,主庫只包含主庫的表,備份庫只包含備份庫的表,多資料配置完成
坑五:hibernate預設配置被覆蓋
若在單資料來源配置下應當無需配置該項,同時發現本應該預設將駝峰屬性名對映為_
拼接名稱策略也失效,說明自定義配置源時覆蓋了預設配置或者導致預設配置失效
查詢原始碼分析原因,發現hibernate
相關配置均在HibernateProperties
配置類中
package org.springframework.boot.autoconfigure.orm.jpa;
/**
* Configuration properties for Hibernate.
*
* @author Stephane Nicoll
* @since 2.1.0
* @see JpaProperties
*/
@ConfigurationProperties("spring.jpa.hibernate")
public class HibernateProperties {}
複製程式碼
而該類在HibernateJpaConfiguration
中進行注入,但配置類中進行了條件裝配註解@ConditionalOnSingleCandidate
標註,導致在多資料來源情況下不滿足該配置,類沒有被裝配到Spring Bean中
package org.springframework.boot.autoconfigure.orm.jpa;
/**
* {@link JpaBaseConfiguration} implementation for Hibernate.
* @since 2.0.0
*/
@Configuration
@EnableConfigurationProperties(HibernateProperties.class)
@ConditionalOnSingleCandidate(DataSource.class)
class HibernateJpaConfiguration extends JpaBaseConfiguration {
private final HibernateProperties hibernateProperties;
HibernateJpaConfiguration(DataSource dataSource, JpaProperties jpaProperties,
ConfigurableListableBeanFactory beanFactory,
ObjectProvider<JtaTransactionManager> jtaTransactionManager,
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers,
HibernateProperties hibernateProperties,
ObjectProvider<Collection<DataSourcePoolMetadataProvider>> metadataProviders,
ObjectProvider<SchemaManagementProvider> providers,
ObjectProvider<PhysicalNamingStrategy> physicalNamingStrategy,
ObjectProvider<ImplicitNamingStrategy> implicitNamingStrategy,
ObjectProvider<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers) {
super(dataSource, jpaProperties, jtaTransactionManager,
transactionManagerCustomizers);
this.hibernateProperties = hibernateProperties;
this.defaultDdlAutoProvider = new HibernateDefaultDdlAutoProvider(providers);
this.poolMetadataProvider = new CompositeDataSourcePoolMetadataProvider(
metadataProviders.getIfAvailable());
this.hibernatePropertiesCustomizers = determineHibernatePropertiesCustomizers(
physicalNamingStrategy.getIfAvailable(),
implicitNamingStrategy.getIfAvailable(), beanFactory,
hibernatePropertiesCustomizers.orderedStream()
.collect(Collectors.toList()));
}
...
}
複製程式碼
解決方法就只能自己工程內實現該類的配置,注意,Spring 2.x版本自動裝配變化較大,當前配置基於最新版2.1.3有效,並不代表對後續版本一定相容
工程配置
package top.fjy8018.jpadatasource.config;
import com.zaxxer.hikari.HikariDataSource;
import org.hibernate.boot.model.naming.ImplicitNamingStrategy;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.cfg.AvailableSettings;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.boot.jdbc.SchemaManagement;
import org.springframework.boot.jdbc.SchemaManagementProvider;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.hibernate5.SpringBeanContainer;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.ClassUtils;
import top.fjy8018.jpadatasource.entity.backup.Order;
import top.fjy8018.jpadatasource.entity.primary.Product;
import top.fjy8018.jpadatasource.repository.backup.OrderRepository;
import top.fjy8018.jpadatasource.repository.primary.ProductRepository;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* @author F嘉陽
* @date 2019-03-30 9:24
*/
@Configuration
public class DataAccessConfig {
private final ObjectProvider<SchemaManagementProvider> providers;
private final HibernateProperties hibernateProperties;
private final JpaProperties properties;
private final ObjectProvider<PhysicalNamingStrategy> physicalNamingStrategy;
private final ObjectProvider<ImplicitNamingStrategy> implicitNamingStrategy;
private final ConfigurableListableBeanFactory beanFactory;
private final ObjectProvider<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers;
@Autowired
public DataAccessConfig(ObjectProvider<SchemaManagementProvider> providers, HibernateProperties hibernateProperties, JpaProperties properties, ObjectProvider<PhysicalNamingStrategy> physicalNamingStrategy, ObjectProvider<ImplicitNamingStrategy> implicitNamingStrategy, ConfigurableListableBeanFactory beanFactory, ObjectProvider<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers) {
this.providers = providers;
this.hibernateProperties = hibernateProperties;
this.properties = properties;
this.physicalNamingStrategy = physicalNamingStrategy;
this.implicitNamingStrategy = implicitNamingStrategy;
this.beanFactory = beanFactory;
this.hibernatePropertiesCustomizers = hibernatePropertiesCustomizers;
}
@Bean
@Primary
@ConfigurationProperties("spring.datasource.first")
public DataSourceProperties firstDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("spring.datasource.first.configuration")
public HikariDataSource firstDataSource() {
return firstDataSourceProperties().initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("spring.datasource.second")
public DataSourceProperties secondDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("spring.datasource.second.configuration")
public HikariDataSource secondDataSource() {
return secondDataSourceProperties().initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean firstEntityManagerFactory(
EntityManagerFactoryBuilder builder, @Qualifier("firstDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
// 加入規約配置
.properties(getVendorProperties(dataSource))
.packages(Product.class)
.persistenceUnit("first")
.build();
}
@Bean
public LocalContainerEntityManagerFactoryBean secondEntityManagerFactory(
EntityManagerFactoryBuilder builder, @Qualifier("secondDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.properties(getVendorProperties(dataSource))
.packages(Order.class)
.persistenceUnit("second")
.build();
}
@Bean
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("firstEntityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory.getObject());
}
@Bean
public PlatformTransactionManager backupTransactionManager(
@Qualifier("secondEntityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory.getObject());
}
@EnableJpaRepositories(basePackageClasses = ProductRepository.class,
entityManagerFactoryRef = "firstEntityManagerFactory", transactionManagerRef = "primaryTransactionManager")
@Primary
public class PrimaryConfiguration {
}
@EnableJpaRepositories(basePackageClasses = OrderRepository.class,
entityManagerFactoryRef = "secondEntityManagerFactory", transactionManagerRef = "backupTransactionManager")
public class secondConfiguration {
}
/**
* 獲取配置檔案資訊
*
* @param dataSource
* @return
*/
private Map<String, Object> getVendorProperties(DataSource dataSource) {
List<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers = determineHibernatePropertiesCustomizers(
physicalNamingStrategy.getIfAvailable(),
implicitNamingStrategy.getIfAvailable(), beanFactory,
this.hibernatePropertiesCustomizers.orderedStream()
.collect(Collectors.toList()));
Supplier<String> defaultDdlMode = () -> new HibernateDefaultDdlAutoProvider(providers)
.getDefaultDdlAuto(dataSource);
return new LinkedHashMap<>(this.hibernateProperties.determineHibernateProperties(
properties.getProperties(),
new HibernateSettings().ddlAuto(defaultDdlMode)
.hibernatePropertiesCustomizers(
hibernatePropertiesCustomizers)));
}
/**
* 命名策略自動判斷
*
* @param physicalNamingStrategy
* @param implicitNamingStrategy
* @param beanFactory
* @param hibernatePropertiesCustomizers
* @return
*/
private List<HibernatePropertiesCustomizer> determineHibernatePropertiesCustomizers(
PhysicalNamingStrategy physicalNamingStrategy,
ImplicitNamingStrategy implicitNamingStrategy,
ConfigurableListableBeanFactory beanFactory,
List<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers) {
List<HibernatePropertiesCustomizer> customizers = new ArrayList<>();
if (ClassUtils.isPresent(
"org.hibernate.resource.beans.container.spi.BeanContainer",
getClass().getClassLoader())) {
customizers
.add((properties) -> properties.put(AvailableSettings.BEAN_CONTAINER,
new SpringBeanContainer(beanFactory)));
}
if (physicalNamingStrategy != null || implicitNamingStrategy != null) {
customizers.add(new NamingStrategiesHibernatePropertiesCustomizer(
physicalNamingStrategy, implicitNamingStrategy));
}
customizers.addAll(hibernatePropertiesCustomizers);
return customizers;
}
/**
* 自動進行建表操作
*/
class HibernateDefaultDdlAutoProvider implements SchemaManagementProvider {
private final Iterable<SchemaManagementProvider> providers;
HibernateDefaultDdlAutoProvider(Iterable<SchemaManagementProvider> providers) {
this.providers = providers;
}
public String getDefaultDdlAuto(DataSource dataSource) {
if (!EmbeddedDatabaseConnection.isEmbedded(dataSource)) {
return "none";
}
SchemaManagement schemaManagement = getSchemaManagement(dataSource);
if (SchemaManagement.MANAGED.equals(schemaManagement)) {
return "none";
}
return "create-drop";
}
@Override
public SchemaManagement getSchemaManagement(DataSource dataSource) {
return StreamSupport.stream(this.providers.spliterator(), false)
.map((provider) -> provider.getSchemaManagement(dataSource))
.filter(SchemaManagement.MANAGED::equals).findFirst()
.orElse(SchemaManagement.UNMANAGED);
}
}
private static class NamingStrategiesHibernatePropertiesCustomizer
implements HibernatePropertiesCustomizer {
private final PhysicalNamingStrategy physicalNamingStrategy;
private final ImplicitNamingStrategy implicitNamingStrategy;
NamingStrategiesHibernatePropertiesCustomizer(
PhysicalNamingStrategy physicalNamingStrategy,
ImplicitNamingStrategy implicitNamingStrategy) {
this.physicalNamingStrategy = physicalNamingStrategy;
this.implicitNamingStrategy = implicitNamingStrategy;
}
/**
* 資料庫命名對映策略
*
* @param hibernateProperties the JPA vendor properties to customize
*/
@Override
public void customize(Map<String, Object> hibernateProperties) {
if (this.physicalNamingStrategy != null) {
hibernateProperties.put("hibernate.physical_naming_strategy",
this.physicalNamingStrategy);
}
if (this.implicitNamingStrategy != null) {
hibernateProperties.put("hibernate.implicit_naming_strategy",
this.implicitNamingStrategy);
}
}
}
}
複製程式碼
配置Bean完成後則規約配置會生效,便無需在配置檔案顯示配置DDL語句執行開關
啟動日誌也可看出結果符合預期
總結
由此發現,在SpringBoot版本遷移過程中不一定互相相容,同時官方文件儘管全面,但隨著版本變化文件也有不準確情況。而如果完全依靠爬原始碼複製原始碼配置也是無法實現最簡單化的配置,需要結合文件、原始碼、日誌逐一分析
工程原始碼已開放,注意配置自己的資料庫連線地址,工程內的連線地址已經失效