springboot+jpa多数据源配置实例

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就可以了。service层就是调用dao层的方法。为了解决hibernate懒加载的问题,我们需要在service层接口上注入@Transaction注解。如果不加注解,可以在配置文件中加入配置:

#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;
}

 

你可能感兴趣的:(java)