springboot+jpa配置多数据源一直都在听说,没有实际动手演练,今天动手一试,发现有一些麻烦,麻烦的地方在于,需要严格区分多种数据源带来的变化,实体需要区分,dao层需要区分,service一般来说是事务控制的入口,既然底层数据来源都不同,service层也是需要严格区分的,所以说controller,service,dao三层架构的系统来说,就需要改变service,dao相关的数据库配置。只有controller可以勉强避免修改。
这里所说的多数据源可以是同一个类型的数据库的两个实例,也可以是不同数据库的两个实例,这里以mysql和postgresql两个数据库为例,来介绍如何做多数据源配置。以及多数据源配置可能带来的问题。
构建maven工程的时候,除了springboot基础依赖,就是spring-boot-starter-data-jpa,以及mysql,postgresql驱动。
org.springframework.boot
spring-boot-starter-parent
2.0.4.RELEASE
junit
junit
test
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
org.postgresql
postgresql
org.projectlombok
lombok
provided
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-maven-plugin
true
true
我们在前面说了从service到dao,再到实体类,既然数据库来源不同,他们在创建的时候就要考虑做区分。使用jpa做数据持久化,我们需要实体管理器EntityManager,而EntityManager需要实体管理器工厂类EntityManagerFactory,而实体管理器工厂类EntityManagerFactory需要数据源DataSource,另外要做事务管理,需要JpaTransactionManager,它也需要数据源,整个配置还是我们在做xml配置的思路一样。
这里我们将mysql配置作为默认数据源primaryDataSource,另外一个数据源我们称之为otherDataSource,使用properties配置文件,这样配置更容易理解,占用篇幅也少,application.properties内容如下:
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false&characterEncoding=UTF-8&useUnicode=true
spring.datasource.username=root
spring.datasource.password=root
#other
spring.datasource.other.driver-class-name=org.postgresql.Driver
spring.datasource.other.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.other.username=postgres
spring.datasource.other.password=
spring.jpa.hibernate.ddl-auto=update
#spring.jpa.open-in-view=true
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.max_fetch_depth=1
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.hbm2ddl=update
#spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.jackson.serialization.fail-on-empty-beans=false
数据源配置类:
package com.xxx.springboot.config;
import javax.sql.DataSource;
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
public class DataSourceConfig {
@Bean(name="dataSource")
@Primary
@ConfigurationProperties(prefix="spring.datasource")
public DataSource dataSource(){
//return DataSourceBuilder.create().build();
return dataSourceProperties().initializeDataSourceBuilder().build();
}
@Primary
@Bean(name="dataSourceProperties")
@ConfigurationProperties(prefix="spring.datasource")
public DataSourceProperties dataSourceProperties(){
return new DataSourceProperties();
}
@Bean(name="otherDataSource")
@ConfigurationProperties(prefix="spring.datasource.other")
public DataSource otherDataSource(){
//return DataSourceBuilder.create().build();
return otherDataSourceProperties().initializeDataSourceBuilder().build();
}
@Bean(name="otherDataSourceProperties")
@ConfigurationProperties(prefix="spring.datasource.other")
public DataSourceProperties otherDataSourceProperties(){
return new DataSourceProperties();
}
}
因为application.properties中指定数据源url使用的属性是url,而不是jdbc-url,因此,这里的数据源配置类,我们没有使用默认的方式创建:
DataSourceBuilder.create().build()
而使用的是:
new DataSourceProperties().initializeDataSourceBuilder().build()
如果不是这么来做,会报错:jdbcUrl is required with driverClassName
我们还需要配置各自的EntityManager,PlatformTransactionManager:
mysql数据源对应的相关配置:
package com.xxx.springboot.config;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
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 org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef="entityManagerFactoryPrimary",
transactionManagerRef="transactionManagerPrimary",
basePackages={"com.xxx.springboot.dao.mysql"})
public class PrimaryDataSourceConfig {
@Autowired
@Qualifier("dataSource")
private DataSource dataSource;
@Autowired
private JpaProperties jpaProperties;
@Primary
@Bean(name="entityManagerPrimary")
public EntityManager entityManager(EntityManagerFactoryBuilder builder){
return entityManagerFactoryBean(builder).getObject().createEntityManager();
}
@Primary
@Bean(name="entityManagerFactoryPrimary")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(EntityManagerFactoryBuilder builder){
return builder
.dataSource(dataSource)
.properties(getProperties())
.packages("com.xxx.springboot.domain.mysql")
.persistenceUnit("primaryPersistentUnit")
.build();
}
public Map getProperties(){
Map map = new HashMap();
map.put("format_sql", "true");
map.put("max_fetch_depth", "1");
return map;
}
@Primary
@Bean(name="transactionManagerPrimary")
public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder){
return new JpaTransactionManager(entityManagerFactoryBean(builder).getObject());
}
}
postgresql数据源对应的相关配置:
package com.xxx.springboot.config;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef="entityManagerFactoryOther",
transactionManagerRef="transactionManagerOther",
basePackages={"com.xxx.springboot.dao.postgresql"})
public class OtherDataSourceConfig {
@Autowired
@Qualifier("otherDataSource")
private DataSource otherDataSource;
@Autowired
private JpaProperties jpaProperties;
@Bean(name="entityManagerOther")
public EntityManager entityManager(EntityManagerFactoryBuilder builder){
return entityManagerFactoryBean(builder).getObject().createEntityManager();
}
@Bean(name="entityManagerFactoryOther")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(EntityManagerFactoryBuilder builder){
return builder
.dataSource(otherDataSource)
.properties(getProperties())
.packages("com.xxx.springboot.domain.postgresql")
.persistenceUnit("otherPersistentUnit")
.build();
}
public Map getProperties(){
Map map = new HashMap();
map.put("format_sql", "true");
map.put("max_fetch_depth", "1");
//map.put("dialect", "org.hibernate.dialect.PostgreSQL9Dialect");
return map;
}
@Bean(name="transactionManagerOther")
public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder){
return new JpaTransactionManager(entityManagerFactoryBean(builder).getObject());
}
}
两个配置类,除了注解不一样之外,代码几乎一样,这就是多数据源配置显得有意思的地方,其实一点也不冗余,否则,就无法彻底分离两个数据源。配置类中,通过配置属性prefix前缀来获取对应的数据源信息。另外,这里有个主配置,默认我们使用的是mysql数据库,因此在mysql相关的配置如:DataSource,EntityManager,EntityManagerFactory,TransactionManager配置上除了@Bean(name="")和@Qualifer()来指定他们的区别之外,还有一个@Primary注解,表示主配置。
配置中我们还通过basePackages属性指定了Repository的位置,也就是dao层接口的位置,还通过packages属性指定了各自实体类的位置。这样,数据源配置清楚了,剩下就是各自数据源对应的实体和dao,service编码了。
dao层很简单,就是一个接口,然后继承JpaRepository
#spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
关于懒加载的问题,还想再说一下,报错一般是这样的:LazyInitializationException:could not initialize proxy [com.xxx....] - no session,原因是,我们在通过dao.getOne()这样的请求,他是返回一个代理,并不是直接查询的数据库,当真正需要使用数据的时候,才会去数据库查询,而这时候,session已经关闭。这个问题很奇怪,如果我们使用的是dao.findById(id)却不会遇到这样的懒加载异常问题,因为它直接取查了数据库,所以底层原因是懒加载导致的。解决问题的办法有以下几种方案:
1、取消懒加载,这个在实体中通过注解@Proxy(lazy=false)来实现。
2、既然session已经关闭,那么我们让session在使用期间一直开启。这就回到了openSessionInView的解决办法了,这种办法在单元测试service方法的时候却不生效,不知道为什么。无论我们配置spring.jpa.open-in-view=true还是在启动类中增加如下代码:
@Bean
public OpenEntityManagerInViewFilter openEntityManagerInViewFilter(){
return new OpenEntityManagerInViewFilter();
}
有人说通过这种办法能够解决,但是对于dao.getOne(id)这个方法来说是不生效的。
3、 这里涉及到两个事务,为了解决这个问题,可以让调用getOne(id)的方法在一个事务中,因此我们可以在调用get(id)->getOne(id)的方法体上加上事务@Transaction,也可以解决问题,尤其是在单元测试的时候,我们可以这么来做:
@Test
@Transactional
public void query(){
User user = userService.get(14);
System.out.println(user);
}
但是这毕竟是单元测试,实际中,调用service层方法的是controller层,我们不可能将事务加载controller层上面。实际上,在真正启动项目进行测试的时候发现,即使controller层不加事务注解,我们访问controller对应的接口,也是没有任何问题的,不会出现懒加载异常,这个一直是我不太理解的地方。
4、这个是一个终极解决办法,就是统一配置hibernate的属性,让系统所有的请求不执行懒加载,而不用在每个实体上通过@Proxy(lazy=false)来注解解决。这个属性就是spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true。在多数据源配置的时候,这个配置我们配置在默认配置文件中application.properties中,只会对主数据源生效,而其他数据源不会生效,我们需要在对数据源进行单独配置。这就是我们数据源配置的部分:
@Bean(name="entityManagerFactoryOther")
public LocalContainerEntityManagerFactoryBean
entityManagerFactoryBean(EntityManagerFactoryBuilder builder){
return builder
.dataSource(otherDataSource)
.properties(getProperties())
.packages("com.xxx.springboot.domain.postgresql")
.persistenceUnit("otherPersistentUnit")
.build();
}
public Map getProperties(){
Map map = new HashMap();
map.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQL9Dialect");
map.put("hibernate.enable_lazy_load_no_trans", "true");
return map;
}