## 前言 一般在Spring Boot 專案中,若只需要連接一個資料庫,只需要依賴`spring-boot-start-jdbc`和配置好`application.yml`即可使用。 但若功能需要讀取複數個不同的資料庫時就需要特別配置DataSource。 ## 作法 **DataBaseConfiguration.java** 在該檔案內定義了2個不同的資料庫DataSource,`env.getProperty("XXX")`取的是`application.yml`設定好的連線資訊、參數。 ```java package com.company.qqq.domain.config; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import java.util.Objects; import java.util.Properties; import java.util.concurrent.TimeUnit; import oracle.jdbc.OracleConnection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.Ordered; import org.springframework.core.env.Environment; @Configuration @ConditionalOnClass(DataSource.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) public class DomainConfiguration { @Autowired private Environment env; public DomainConfiguration(Environment env) { this.env = env; } @Primary @Bean(name = "oracleDataSource") public DataSource brddhsDataSource() { HikariDataSource hikariDataSource = new HikariDataSource(); hikariDataSource.setJdbcUrl(env.getProperty("spring.datasource.oracle.hikari.jdbc-url")); hikariDataSource.setDriverClassName(Objects.requireNonNull(env.getProperty("spring.datasource.oracle.hikari.driver-class-name"))); hikariDataSource.setUsername(Objects.requireNonNull(env.getProperty("spring.datasource.oracle.hikari.username"))); hikariDataSource.setPassword(Objects.requireNonNull(env.getProperty("spring.datasource.oracle.hikari.pd"))); hikariDataSource.setConnectionTimeout(Long.parseLong(Objects.requireNonNull(env.getProperty("spring.datasource.oracle.hikari.connection-timeout")))); hikariDataSource.setIdleTimeout(Long.parseLong(Objects.requireNonNull(env.getProperty("spring.datasource.oracle.hikari.idle-timeout")))); hikariDataSource.setMaximumPoolSize(Integer.parseInt(Objects.requireNonNull(env.getProperty("spring.datasource.oracle.hikari.maximum-pool-size")))); hikariDataSource.setMaxLifetime(Long.parseLong(Objects.requireNonNull(env.getProperty("spring.datasource.oracle.hikari.max-lifetime")))); final Properties properties = new Properties(); properties.setProperty(OracleConnection.CONNECTION_PROPERTY_THIN_NET_CONNECT_TIMEOUT, String.valueOf(TimeUnit.SECONDS.toMillis(600))); properties.setProperty(OracleConnection.CONNECTION_PROPERTY_THIN_READ_TIMEOUT, String.valueOf(TimeUnit.SECONDS.toMillis(600))); hikariDataSource.setDataSourceProperties(properties); return hikariDataSource; } @Bean(name = "mssqlDataSource") public DataSource mssqlDataSource() { HikariDataSource hikariDataSource = new HikariDataSource(); hikariDataSource.setJdbcUrl(env.getProperty("spring.datasource.mssql.hikari.jdbc-url")); hikariDataSource.setDriverClassName(Objects.requireNonNull(env.getProperty("spring.datasource.mssql.hikari.driver-class-name"))); hikariDataSource.setUsername(Objects.requireNonNull(env.getProperty("spring.datasource.mssql.hikari.username"))); hikariDataSource.setPassword(Objects.requireNonNull(env.getProperty("spring.datasource.mssql.hikari.pd"))); hikariDataSource.setConnectionTimeout(Long.parseLong(Objects.requireNonNull(env.getProperty("spring.datasource.mssql.hikari.connection-timeout")))); hikariDataSource.setIdleTimeout(Long.parseLong(Objects.requireNonNull(env.getProperty("spring.datasource.mssql.hikari.idle-timeout")))); hikariDataSource.setMaximumPoolSize(Integer.parseInt(Objects.requireNonNull(env.getProperty("spring.datasource.mssql.hikari.maximum-pool-size")))); hikariDataSource.setMaxLifetime(Long.parseLong(Objects.requireNonNull(env.getProperty("spring.datasource.mssql.hikari.max-lifetime")))); return hikariDataSource; } } ``` 接著為Oracle、MSSQL的DataSource分別建立自己的`EntityManagerFactory`和`TransactionManager`。 其中最重要的是`@EnableJpaRepositories`: * `basePackages`:寫的包名就是當JPA存取到該包名底下的Entity時會使用該config的設定,也就是說不同資料庫的Entity需要分別放在不同package底下。 * `entityManagerFactoryRef` :使用的是需要使用哪個entityManagerFactory實體。 * `transactionManagerRef` :使用的是需要使用哪個transactionManager實體。 ) **OracleConfiguration.java** ```java package com.company.qqq.domain.config; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.PersistenceContext; import javax.sql.DataSource; import java.util.Properties; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.jdbc.JdbcProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.Ordered; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @ConditionalOnClass(DataSource.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @EnableJpaRepositories( basePackages = "com.company.qqq.domain.oracle", entityManagerFactoryRef = "oracleFactory", transactionManagerRef = "oracleManager" ) @EnableAsync public class OracleConfiguration { @Autowired @Qualifier("oracleDataSource") private DataSource dataSource; @Primary @Bean(name = "oracleFactory") public LocalContainerEntityManagerFactoryBean managerFactory() { final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource); em.setPackagesToScan("com.company.domain.entity.oracle"); em.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); em.setPersistenceUnitName("oracle"); final Properties properties = new Properties(); properties.setProperty("javax.persistence.query.timeout", String.valueOf(TimeUnit.SECONDS.toMillis(600))); properties.setProperty("javax.persistence.lock.timeout", String.valueOf(TimeUnit.SECONDS.toMillis(600))); em.setJpaProperties(properties); return em; } @Primary @Bean(name = "oracleManager") JpaTransactionManager transactionManager(@Qualifier("oracleFactory") final EntityManagerFactory entityManagerFactory) { final JpaTransactionManager queryDataTransactionManager = new JpaTransactionManager(); queryDataTransactionManager.setEntityManagerFactory(entityManagerFactory); return queryDataTransactionManager; } @Primary @Bean @PersistenceContext(unitName = "oracle") public JPAQueryFactory jpaQueryFactory(@Qualifier("oracleFactory") EntityManager entityManager) { return new JPAQueryFactory(entityManager); } @Primary @Bean public JdbcTemplate jdbcTemplate(@Qualifier("oracleDataSource") DataSource dataSource, JdbcProperties properties) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); JdbcProperties.Template template = properties.getTemplate(); jdbcTemplate.setFetchSize(template.getFetchSize()); jdbcTemplate.setMaxRows(template.getMaxRows()); if (template.getQueryTimeout() != null) { jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds()); } return jdbcTemplate; } } ``` **MssqlConfiguration.java** ```java package com.company.qqq.domain.config; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.PersistenceContext; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.jdbc.JdbcProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; @Configuration @ConditionalOnClass(DataSource.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @EnableJpaRepositories( basePackages = "com.company.qqq.domain.mssql", entityManagerFactoryRef = "mssqlFactory", transactionManagerRef = "mssqlManager" ) public class MssqlConfiguration { @Autowired @Qualifier("mssqlDataSource") private DataSource dataSource; public MssqlConfiguration(DataSource dataSource) { this.dataSource = dataSource; } @Bean(name = "mssqlFactory") public LocalContainerEntityManagerFactoryBean managerFactory() { final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource); em.setPackagesToScan("com.company.domain.entity.mssql"); em.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); em.setPersistenceUnitName("mssql"); return em; } @Bean(name = "mssqlManager") JpaTransactionManager transactionManager(@Qualifier("mssqlFactory") final EntityManagerFactory entityManagerFactory) { final JpaTransactionManager queryDataTransactionManager = new JpaTransactionManager(); queryDataTransactionManager.setEntityManagerFactory(entityManagerFactory); return queryDataTransactionManager; } @Bean public JdbcTemplate mssqlJdbcTemplate(@Qualifier("mssqlDataSource") DataSource dataSource, JdbcProperties properties) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); JdbcProperties.Template template = properties.getTemplate(); jdbcTemplate.setFetchSize(template.getFetchSize()); jdbcTemplate.setMaxRows(template.getMaxRows()); if (template.getQueryTimeout() != null) { jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds()); } return jdbcTemplate; } } ``` 設定好之後,使用JPA的查詢就會自動配對好對應的資料庫。 但是若要客製特殊SQL語法的查詢實作時,就需要另外的作法。 **TableARepository.java (原本)** 這是原本的Repository類別,只要繼承JpaRepository就可以使用JPA預設的查詢。 ```java package com.company.qqq.oracle.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.company.qqq.entity.oracle; @Repository public interface TableARepository extends JpaRepository<TableA, Integer> { public List<TableA> queryAll(); } ``` **TableARepository.java (客製)** 多繼承一個`TableARepositoryCustom`介面,該介面定義了客製的方法。 並由`TableARepositoryImpl`來實作裡面的方法,該類別內指定使用哪個`EntityManager`去存取哪個資料庫。並定義了需要的SQL語句就可以使用JPA客製的查詢。 ```java package com.company.qqq.oracle.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.company.qqq.entity.oracle; @Repository public interface TableARepository extends JpaRepository<TableA, Integer>, TableARepositoryCustom { public List<TableA> queryAll(); } ``` **TableARepositoryCustom.java** ```java package com.company.qqq.oracle.repository.coustom; import com.company.qqq.oracle.entity.tableA; import com.company.qqq.oracle.repository.custom.TableARepositoryCustom; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface TableARepositoryCustom { List<TableADTO> getMethod1(String idn, String kind); List<TableADTO> getMethod2(String idn, String seq); List<TableADTO> getMethod3(String account); ``` **TableARepositoryImpl.java** ```java package com.company.qqq.oracle.repository.impl; import com.company.qqq.oracle.repository.custom.TableARepositoryCustom; import com.company.qqq.oracle.dto.TableADTO; import lombok.AllArgsConstructor; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaContext; public class TableARepositoryImpl implements TableARepositoryCustom { private final EntityManager em; @Autowired public TableADaoImpl(JpaContext context) { this.em = context.getEntityManagerByManagedType(TableA.class); } @Override public List<TableADTO> getMethod1(String idn, String kind) { String jpql = " SELECT a FROM TableA where idn = " + idn + " " and kind = " " + kind ; TypedQuery<TableA> query = this.em.createQuery(jpql, TableA.class); return query.getResultList(); } @Override public List<TableADTO> getMethod2(String idn, String seq) { //TODO } @Override public List<TableADTO> getMethod3(String account) { //TODO } } ```