美文网首页技术干货程序员Spring Boot
springboot项目使用spring-data-jpa如何连

springboot项目使用spring-data-jpa如何连

作者: 低调的微胖 | 来源:发表于2018-06-07 21:36 被阅读44次

    一. 为什么要连接多数据源

    springboot下使用spring-data-jpa连接数据库配置非常方便,只需要在application.properties简单的几行配置就能搞定。
    有些时候我们需要在一个项目里面连接多个数据库,如常见的数据库主从分离,将部分查询请求分流到只读从库里,降低主库的压力。
    这种时候,就不能通过简单的几行配置来搞定了;需要手动进行一些配置才行。

    二. springboot下连接多数据源的两种方案

    目前有两种方案可以解决这个问题

    1. 为每个数据源配置一套dataSource,并针对每个dataSource配置一套jpa和事务管理器。
    2. 为每个数据源配置一套dataSource,使用AbstractRoutingDataSource将所有数据源集成到一起成为动态数据源,在代码调用的时候随时切换数据源。

    这两套方案都可以满足日常使用需要,各位看官可以根据个人喜好选用。

    三. 配置多套entityManagerFactory

    先来讲解为每个数据源配置一套dataSource,并针对每个dataSource配置一套jpa和事务管理器的方案。废话不多说,直接上代码。具体样例代码点此查看

    首先,我们要有两个数据库。在application.properties中如下配置。

    #数据库通用配置
    spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
    spring.datasource.hikari.maximum-pool-size=5
    spring.jpa.database=MYSQL
    spring.jpa.hibernate.dll-auto=none
    spring.jpa.show-sql=true
    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
    #主库配置
    spring.datasource.primary.url=jdbc:mysql://wuxiaodong.mysql.rds.aliyuncs.com:3306/test_for_blog?serverTimezone=GMT%2B8
    spring.datasource.primary.username=test_for_blog
    spring.datasource.primary.password=A1b2c3d4e5
    #二库配置
    spring.datasource.secondary.url=jdbc:mysql://wuxiaodong.mysql.rds.aliyuncs.com:3306/test_for_blog2?serverTimezone=GMT%2B8
    spring.datasource.secondary.username=test_for_blog
    spring.datasource.secondary.password=A1b2c3d4e5
    

    主库配置

    @Configuration
    @EnableTransactionManagement
    @EnableJpaRepositories(entityManagerFactoryRef="entityManagerFactoryPrimary", transactionManagerRef="transactionManagerPrimary", basePackages= {"com.test.dao.primary"})
    public class DataSourcePrimaryConfig
    {
        @Value("${spring.datasource.driverClassName}")
        private String driverClassName;
    
        @Value("${spring.datasource.hikari.maximum-pool-size}")
        private Integer maximumPoolSize;
    
        @Value("${spring.datasource.primary.url}")
        private String primaryUrl;
    
        @Value("${spring.datasource.primary.username}")
        private String primaryUsername;
    
        @Value("${spring.datasource.primary.password}")
        private String primaryPassword;
    
        /**
         * 主库数据源配置
         * @return
         */
        @Primary
        @Bean(name = "dataSourcePrimary")
        public DataSource dataSourcePrimary()
        {
            HikariDataSource dataSourcePrimary = new HikariDataSource();
            dataSourcePrimary.setDriverClassName(driverClassName);
            dataSourcePrimary.setJdbcUrl(primaryUrl);
            dataSourcePrimary.setUsername(primaryUsername);
            dataSourcePrimary.setPassword(primaryPassword);
            dataSourcePrimary.setMaximumPoolSize(maximumPoolSize);
    
            return dataSourcePrimary;
        }
    
        /**
         * 主库jpa 实例管理器工厂配置
         */
        @Primary
        @Bean(name = "entityManagerFactoryPrimary")
        public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary(EntityManagerFactoryBuilder builder)
        {
            LocalContainerEntityManagerFactoryBean em = builder
                    .dataSource(dataSourcePrimary())
                    .packages("com.test.model")
                    .build();
            Properties properties = new Properties();
            properties.setProperty("hibernate.physical_naming_strategy", "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
            em.setJpaProperties(properties);
            return em;
        }
    
        /**
         * 主库事务管理器配置
         */
        @Primary
        @Bean(name = "transactionManagerPrimary")
        public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder)
        {
            JpaTransactionManager txManager = new JpaTransactionManager();
            txManager.setEntityManagerFactory(entityManagerFactoryPrimary(builder).getObject());
            return txManager;
        }
    }
    

    第二数据库配置

    @Configuration
    @EnableTransactionManagement
    @EnableJpaRepositories(entityManagerFactoryRef="entityManagerFactorySecondary", transactionManagerRef="transactionManagerSecondary", basePackages= {"com.test.dao.secondary"})
    public class DataSourceSecondaryConfig
    {
        @Value("${spring.datasource.driverClassName}")
        private String driverClassName;
    
        @Value("${spring.datasource.hikari.maximum-pool-size}")
        private Integer maximumPoolSize;
    
        @Value("${spring.datasource.secondary.url}")
        private String secondaryUrl;
    
        @Value("${spring.datasource.secondary.username}")
        private String secondaryUsername;
    
        @Value("${spring.datasource.secondary.password}")
        private String secondaryPassword;
    
        /**
         * 二库数据源配置
         * @return
         */
        @Bean(name = "dataSourceSecondary")
        public DataSource dataSourceSecondary()
        {
            HikariDataSource dataSourceSecondary = new HikariDataSource();
            dataSourceSecondary.setDriverClassName(driverClassName);
            dataSourceSecondary.setJdbcUrl(secondaryUrl);
            dataSourceSecondary.setUsername(secondaryUsername);
            dataSourceSecondary.setPassword(secondaryPassword);
            dataSourceSecondary.setMaximumPoolSize(maximumPoolSize);
    
            return dataSourceSecondary;
        }
    
        /**
         * 二库jpa 实例管理器工厂配置
         */
        @Bean(name = "entityManagerFactorySecondary")
        public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary(EntityManagerFactoryBuilder builder)
        {
            LocalContainerEntityManagerFactoryBean em = builder
                    .dataSource(dataSourceSecondary())
                    .packages("com.test.model")
                    .build();
            Properties properties = new Properties();
            properties.setProperty("hibernate.physical_naming_strategy", "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
            em.setJpaProperties(properties);
            return em;
        }
    
        /**
         * 二库事务管理器配置
         */
        @Bean(name = "transactionManagerSecondary")
        public PlatformTransactionManager transactionManagerSecondary(EntityManagerFactoryBuilder builder)
        {
            JpaTransactionManager txManager = new JpaTransactionManager();
            txManager.setEntityManagerFactory(entityManagerFactorySecondary(builder).getObject());
            return txManager;
        }
    }
    

    大部分代码相信各位看官一眼就能看明白,不过有几个关键单还是要额外说明下。

    @EnableJpaRepositories(entityManagerFactoryRef="entityManagerFactoryPrimary", 
            transactionManagerRef="transactionManagerPrimary", 
            basePackages= {"com.test.dao.primary"})
    

    这里是指定使用我们自定义的jpa实体管理工厂entityManagerFactoryPrimary,事务管理器transactionManagerPrimary来自定义jpa实现。这个jpa只扫描com.test.dao.primary这个包下的Repository。也就是com.test.dao.primary这个包下面的JpaRepository使用我们在配置文件中定义的主库。

    主库配置中,我们定义的几个bean都加上了@Primary注解;而二库的配置中,并没有加上。这是因为spring中,dataSourceentityManagerFactorytransactionManager这几个如果初始化的时候实例化了多个,项目直接无法启动,会给出如下这样的提示

    Parameter 0 of constructor in org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration required a single bean, but 2 were found:
        - dataSourcePrimary: defined by method 'dataSourcePrimary' in class path resource [com/test/config/DataSourcePrimaryConfig.class]
        - dataSourceSecondary: defined by method 'dataSourceSecondary' in class path resource [com/test/config/DataSourceSecondaryConfig.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
    

    但是如果你把主库和二库都加上了@Primary,又会给出这样的错误提示。

    No qualifying bean of type 'javax.sql.DataSource' available: more than one 'primary' bean found among candidates: [dataSourcePrimary, dataSourceSecondary]
    

    dataSourceentityManagerFactorytransactionManager这几个如果必须实例化多个的话,必须使用使用@Primary指定其中一个为默认值。一般,建议使用主库为默认数据源。

    当我们需要使用事务的时候,单数据源的时候,是直接使用@Transactional。但是因为我们配置了多数据源,然后配置主库事务管理器的时候,加上了@Primary的将其指定为默认事务管理器。所有这时候使用@Transactional其实是开启了主库的事务,如果你这时候试图对二库进行事务管理,会发现完全不会生效。
    如果你希望对二库进行事务管理,需要指定使用二库的事务管理器@Transactional(transactionManager="transactionManagerSecondary")
    代码如下

    @Transactional
        public void updateTestTable1()
        {
            TestTable testTable = new TestTable();
            testTable.setName("test");
            testTable.setStatus(1);
            testTablePrimaryRepository.save(testTable);
    
            testTable = testTablePrimaryRepository.findOne(1l);
            testTable.setName("1");
            testTablePrimaryRepository.save(testTable);
        }
    
        @Transactional(transactionManager="transactionManagerSecondary")
        public void updateTestTable2()
        {
            TestTable testTable = new TestTable();
            testTable.setName("test");
            testTable.setStatus(1);
            testTableSecondaryRepository.save(testTable);
    
            testTable = testTableSecondaryRepository.findOne(1l);
            testTable.setName("1");
            testTableSecondaryRepository.save(testTable);
        }
    

    四. 使用AbstractRoutingDataSource实现动态数据源切换

    下面来讲解动态数据源的方案。废话不多说,继续上代码。具体样例代码点此查看

    public class DBContextHolder {
    
        /**
         * 动态数据源key holder
         */
        private static ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    
        public static final String DB_TYPE_PRIMARY = "dataSourceKeyPrimary";
        public static final String DB_TYPE_SECONDARY = "dataSourceKeySecondary";
    
        public static String getDbType() {
            String db = contextHolder.get();
            if (db == null) {
                db = DB_TYPE_PRIMARY;// 默认是主库
            }
            return db;
        }
    
        /**
         * 设置本线程的dbtype
         */
        public static void setDbType(String str) {
            contextHolder.set(str);
        }
    
        /**
         * 清理连接类型
         */
        public static void clearDBType() {
            contextHolder.remove();
        }
    }
    
    public class DynamicDataSource extends AbstractRoutingDataSource
    {
        @Override
        protected Object determineCurrentLookupKey()
        {
            String dbType = DBContextHolder.getDbType();
            return dbType;
        }
    }
    
    @Configuration
    public class DataSourceConfig
    {
        @Value("${spring.datasource.driverClassName}")
        private String driverClassName;
    
        @Value("${spring.datasource.hikari.maximum-pool-size}")
        private Integer maximumPoolSize;
    
        @Value("${spring.datasource.primary.url}")
        private String primaryUrl;
    
        @Value("${spring.datasource.primary.username}")
        private String primaryUsername;
    
        @Value("${spring.datasource.primary.password}")
        private String primaryPassword;
    
        @Value("${spring.datasource.secondary.url}")
        private String secondaryUrl;
    
        @Value("${spring.datasource.secondary.username}")
        private String secondaryUsername;
    
        @Value("${spring.datasource.secondary.password}")
        private String secondaryPassword;
    
        @Bean(name = "dataSource")
        public DataSource dynamicDataSource()
        {
            //配置主库数据源
            HikariDataSource dataSourcePrimary = new HikariDataSource();
            dataSourcePrimary.setDriverClassName(driverClassName);
            dataSourcePrimary.setJdbcUrl(primaryUrl);
            dataSourcePrimary.setUsername(primaryUsername);
            dataSourcePrimary.setPassword(primaryPassword);
            dataSourcePrimary.setMaximumPoolSize(maximumPoolSize);
    
            //配置二库数据源
            HikariDataSource dataSourceSecondary = new HikariDataSource();
            dataSourceSecondary.setDriverClassName(driverClassName);
            dataSourceSecondary.setJdbcUrl(secondaryUrl);
            dataSourceSecondary.setUsername(secondaryUsername);
            dataSourceSecondary.setPassword(secondaryPassword);
            dataSourceSecondary.setMaximumPoolSize(maximumPoolSize);
    
            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put("dataSourceKeyPrimary", dataSourcePrimary);
            targetDataSources.put("dataSourceKeySecondary", dataSourceSecondary);
    
            //配置动态数据源
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            dynamicDataSource.setTargetDataSources(targetDataSources);
            return dynamicDataSource;
        }
    }
    

    下面讲解一下重要的代码。动态数据源方案的核心是spring的抽象类AbstractRoutingDataSource。将多个数据源配置到自定义的动态数据源中

            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put("dataSourceKeyPrimary", dataSourcePrimary);
            targetDataSources.put("dataSourceKeySecondary", dataSourceSecondary);
    
            //配置动态数据源
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            dynamicDataSource.setTargetDataSources(targetDataSources);
    

    在动态数据源类中,要重写一个方法,来告诉每次调用动态数据源的时候,使用哪个key对应的数据源。

    @Override
        protected Object determineCurrentLookupKey()
        {
            String dbType = DBContextHolder.getDbType();
            return dbType;
        }
    

    DBContextHolder中,我们使用ThreadLocal来针对每个线程使用哪个数据源来进行控制。默认使用主库的数据源。如果需要进行切换,如下代码进行切换。

        public List<TestTable> getTestTables2()
        {
            //切换数据源至二库
            DBContextHolder.setDbType(DBContextHolder.DB_TYPE_SECONDARY);
            List<TestTable> testTables = testTableRepository.findAll();
    
            return testTables;
        }
    

    这套动态数据源切换方案,在使用jdbcTemplateMybatis的时候非常好用;但是配合jpa的时候,却发现个问题。jpa在一个线程中拿过一个数据源后,后续使用就一直用那个数据源,即使你加上切换数据源的代码要求切换,但因为jpa根本就没有走动态数据源获取第二次,所以根本切换不了。

        public List<TestTable> getTestTables3()
        {
            //拿到默认数据源,即主库数据源
            List<TestTable> testTables1 = testTableRepository.findAll();
            //要求切换到二库数据源
            DBContextHolder.setDbType(DBContextHolder.DB_TYPE_SECONDARY);
            //因为jpa只拿一次数据源,所以这里依然沿用上一个数据源,即主库数据源
            List<TestTable> testTables2 = testTableRepository.findAll();
    
            List<TestTable> testTables = new ArrayList<>();
            testTables.addAll(testTables1);
            testTables.addAll(testTables2);
    
            return testTables;
        }
    

    五. 两套方案各自的应用场景
    动态数据源方案,配置完成后,只需要简单加上一行代码就可以随意切换使用哪个数据源,不需要对原有代码结构进行很大的变动。为优先考虑方案。但因为在jpahibernate中,框架帮我们做了很多事,有时候数据源并不能自由的切换。所以,建议如下:

    • jdbcTemplatemybaits框架下,推荐使用动态数据源方案。
    • jpahibernate框架下,推荐使用配置多套entityManagerFactory的方案。

    相关文章

      网友评论

        本文标题:springboot项目使用spring-data-jpa如何连

        本文链接:https://www.haomeiwen.com/subject/llzeattx.html