iT邦幫忙

0

VScode 開發應用系統專案(3) - Spring boot 多資料庫支援的配置

  • 分享至 

  • xImage
  •  

Spring boot 配置多資料庫支援

概述

通常大多數專案,配置一個專用的資料庫即可運作,簡單設定application-properties,交由Spring Boot的 AutoConfiguration系統自動連線資料庫。若需要一個資料庫以上,目前學到的方式為以Java Configuration,分別設定連線資料庫。這裡將以三個資料庫作(Oracle、MS Sql、 postgresql)為案例,希望以最少的調整程式而以參數,擴充n個資料庫都沒問題。
因為資料庫安裝與架設,應該由系統管理這負責,系統開發人員只需要申請帳號權限連線使用,如果有需要驗證使用資料庫,請聯繫我開一個資料庫帳號給您,或下載安裝Developer版的資料庫,例如參考 URL: https://www.microsoft.com/zh-tw/sql-server/sql-server-downloads 下載設定架設驗證。

Datasource Configuration 整合了JPA Entity 與 Mybatis設定功能,方便既有Mybatis專案轉換。

準備與檢核

  1. 建置Spring Boot專案後系統自動產生了 application.properties。
  1. 工具類程式已經準備好可以使用。

增加database相關以功能分類放置的 Package,

  1. 集中資料庫相關設定與物件,放置 專案跟目錄下的 database 目錄 (tw.lewishome.webapp.database)

  2. 所有Database共用的 Audit物件

    • tw.lewishome.webapp.database.audit
  3. 依database 分類(primary、secondary、tertiary)區分不同資料庫連線

    • tw.lewishome.webapp.database.primary
    • tw.lewishome.webapp.database.secondary
    • tw.lewishome.webapp.database.tertiary
  4. 每個資料庫連線各功能物件區分(以 primary 為例)

    • tw.lewishome.webapp.database.primary.configuration (資料庫連線相關設定)
    • tw.lewishome.webapp.database.primary.entity (資料庫Table物件,通稱為Entity)
    • tw.lewishome.webapp.database.primary.mybatis (Mybatis 相關物件 )
    • tw.lewishome.webapp.database.primary.repository (JPA 資料庫存取相關物件,通稱為 Repositort) )
    • tw.lewishome.webapp.database.primary.specification (預設動態查詢條件設定 通稱為 Specification)

*檢核建置完成的database專案目錄:
https://ithelp.ithome.com.tw/upload/images/20251114/20139477OhX67OpTI7.png

新建置Mybatis的 AuditInterceptor 設定

tw.lewishome.webapp.database.audit Pakcage內新增AuditInterceptor.java,內容專為Mybatis 新增或更改資料時,自動完成Auidt欄位

  • AuditInterceptor.java程式 (configuration 會使用)
package tw.lewishome.webapp.database.audit;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

// import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import org.springframework.stereotype.Component; 

@Intercepts({
    @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }),
    @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class }),
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Component
public class AuditInterceptor implements Interceptor {

    @SuppressWarnings("rawtypes")
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        // String sqlCommandType = mappedStatement.getSqlCommandType().name();
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        if (SqlCommandType.DELETE == sqlCommandType) {
            return invocation.proceed();
        }
        // String userId = UserContext.getCurrentUserId(); // From ThreadLocal or SecurityContext
        String userId = "system"; // Default or fallback user ID
        Date now = new Date();

        // Handle single entity
        if (parameter instanceof EntityAudit) {
            handleEntity((EntityAudit) parameter, sqlCommandType, userId, now);
        }
        // Handle collections (e.g., List<EntityAudit>)
        else if (parameter instanceof Collection) {
            Collection<?> collection = (Collection<?>) parameter;
            for (Object item : collection) {
                if (item instanceof EntityAudit) {
                    handleEntity((EntityAudit) item, sqlCommandType, userId, now);
                }
            }
        }
        // Handle Map (MyBatis might wrap the parameter in a Map)
        else if (parameter instanceof Map) {
            Map<?, ?> paramMap = (Map<?, ?>) parameter;
            for (Object value : paramMap.values()) {
                if (value instanceof Collection) {
                    Collection<?> collection = (Collection<?>) value;
                    for (Object item : collection) {
                        if (item instanceof EntityAudit) {
                            handleEntity((EntityAudit) item, sqlCommandType, userId, now);
                        }
                    }
                } else if (value instanceof EntityAudit) {
                    handleEntity((EntityAudit) value, sqlCommandType, userId, now);
                }
            }
        }

        // Field[] fields = parameter.getClass().getDeclaredFields();
        // for (Field field : fields) {
        //     field.setAccessible(true); // Allow access to private fields
        //     System.out.println("Field Name: " + field.getName() + ", Value: " + field.get(parameter));
        //     field.setAccessible(false); // Revoke access
        // }

        return invocation.proceed();
    }

    @SuppressWarnings("rawtypes")
    private void handleEntity(EntityAudit entity, SqlCommandType sqlCommandType, String userId, Date now) {
         
        if (SqlCommandType.INSERT == sqlCommandType) {
            entity.setCreatedDate(now);
            // entity.setCreatedBy(userId);
            entity.setLastModifiedDate(now); // Optional: set on insert
            // entity.setUpdatedBy(userId);
        } else   if (SqlCommandType.UPDATE == sqlCommandType) {
            entity.setLastModifiedDate(now); // Optional: set on insert
            // entity.setUpdatedBy(userId);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // No properties needed
    }
}

建置Primary Datasource (資料庫)設定

1. 增加database設置 (application.properties相關新增與調整)

VScode 開發應用系統專案(1)-啟動Spring Boot Web專案 https://ithelp.ithome.com.tw/articles/10398435 時,已經準備好了application.properties,這裡增加一個 spring.profiles.active參數,以便區分專案啟動時,各種不同環境使用不同的資料庫資連線,通常個人PC的開發環境(dev)的資料庫會自行設定連線,但測試(uat)或正式環境(prod)應由系統管理員設置連線JNDI於 web Server(如Jboss),所以專案架構上,需要設定多個啟動參數,依執行環境,系統自動取得資料庫連線參數並啟動。以下是遵循Spring boot的啟動設計:

  • application.properties 增加 spring.profiles.active=dev 作為開發環境的profile參數,測試(uat)或正式環境(prod)將會由系統外的web server(Jboss)或作業系統環境變數覆蓋為 "uat" 或 "prod"等

*注意本專案的資料庫連線參數有設計名稱規則,以 "primary"或 "secondary" 前置區分資料庫的url、user、password等其他連線參數名稱。但使用JNDI則依系統管理規則,沒有限制。

  • application.properties,內容專為全部環境通用的相關資訊
spring.application.name=webapp

# 這裡設定跟執行環境無關的專案的profile
# Sprint Boot 會以spring.profiles.active變數,尋找application-${spring.profiles.active}.properties
# 以下是設定會自動找尋 application-dev.properties載入設定(server會另外設定,覆蓋此變數)
spring.profiles.active=dev

#預設只開啟 primary datasource,後續由 application-dev.properties在打開並設置連線資訊。
primary.datasource.enabled=true
secondary.datasource.enabled=false
tertiary.datasource.enabled=false
  • application-dev.properties,內容專為開發環境適用的相關資訊(如資料庫連線)
#各類資料庫的 url 參考:
# H2 database ==> jdbc:h2:mem:testdb
# SQL-Server  ==> jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]]
#  example    ==> jdbc:sqlserver://sql.lewis-home.tw:1433;databasename=MBS;encrypt=false
# MySQL       ==> jdbc:mysql://[hosts][:portNumber][/database]
#  example    ==> jdbc:mysql://mysql.lewis-home.tw:33060
# AS400(Jt400)==> jdbc:as400://[hosts][;property=value]  
#  example    ==> jdbc:as400://as400system;transaction isolation=none;translate binary=true;date format=iso;prompt=false
# Oracle      ==> jdbc:oracle:thin:@[HOST][:PORT]:SID or jdbc:oracle:thin:@//[HOST][:PORT]/SERVICE
#  example    ==> jdbc:oracle:thin:@oracle.lewis-home.tw:1521:oracle.lewis-home.tw
# postgres    ==> jdbc:postgresql://@[netloc][:port][/dbname][?param1=value1&...]
#  example    ==> jdbc:postgresql://postsql.lewis-home.tw:5432/database

#各類資料庫的 driver class Name
# H2 database ==> org.h2.Driver
# SQL-Server  ==> com.microsoft.sqlserver.jdbc.SQLServerDriver
# MySQL       ==> com.mysql.jdbc.Driver
# AS400(Jt400)==> com.ibm.as400.access.AS400JDBCDriver
# Oracle      ==> oracle.jdbc.driver.OracleDriver
# postgres    ==> org.postgresql.Driver

#Store primary Datasource (這些是自訂的變數名稱,只要與程式內取用的設定一致即可)
primary.datasource.enabled=true
primary.datasource.jdbcurl=jdbc:sqlserver://sql.lewishome.tw:1433;databasename=DbMuserXX1;encrypt=false;characterEncoding=utf-8
primary.datasource.username=MuserXX1
# Database connection password 建議不要存在此駔,使用 Keystore(安全線以及後續密碼交出去給資管理部)
primary.datasource.password=XXXXXXXX (可以聯繫我提供驗證用的帳密)
primary.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
primary.datasource.hibernate.hbm2ddl.auto=update

#Secondary Datasource 是否有需要
secondary.datasource.enabled=true
secondary.datasource.jdbcurl=jdbc:oracle:thin:@oracle.lewishome.tw:1521/orclpdb.lewishome.tw
secondary.datasource.username=OUSERXX1
# Database connection password 建議不要存在此駔,使用 Keystore(安全線以及後續密碼交出去給資管理部)
secondary.datasource.password=XXXXX (可以聯繫我提供驗證用的帳密)
secondary.datasource.driverClassName=oracle.jdbc.OracleDriver
secondary.datasource.hibernate.hbm2ddl.auto=update

  • application-uat.properties,內容增加專為測試環境適用資料庫連線資訊,以後介紹web server時再說明。
#Store primary Datasource (這些是自訂的變數名稱,只要與程式內取用的設定一致即可)
#primary Datasource (using JNDI)
primary.datasource.jndi=java:jboss/datasources/sqlserverDS
primary.datasource.hibernate.hbm2ddl.auto=update

2. 新增Primary資料庫常數設定File (package-info.java)

於 tw.lewishome.webapp.database.primary.configuration內,新增 File: package-info.java,主要是用於primary資料庫常數設定,以及關於此Package的 Javadoc說明,

a. Vscode 新增File(若選新增Class,會因檔名有 '-' 所以會錯誤無法新增)
https://ithelp.ithome.com.tw/upload/images/20251114/20139477RYDuW4DHOq.png
b. 輸入File名稱(.java不能省略)
https://ithelp.ithome.com.tw/upload/images/20251114/20139477nMpgMUyTwT.png

c. 程式碼 package-info.java
這裡的變數,只對package內的程式有效,其他Package(包含子目錄)都無效,後續設定第多個(secondary、tertiary) 的資料庫設定時使用相同變數名稱,一律取PkgConst.xxx的變數,方便複製產生新設定。


/**
 * 此套件包含與資料庫設定相關的組態類別與元件。
 *
 * 主要負責資料庫連線、資料來源管理、以及相關的組態設定,
 * 以確保應用程式能夠正確且有效率地與資料庫進行互動。
 *
 *
 * 建議於本套件中實作資料庫連線池、事務管理、以及資料庫初始化等功能,
 * 以提升系統穩定性與維護性。
 *
 */
package tw.lewishome.webapp.database.primary.configuration;

/**
 * 用於管理主要資料庫相關常數的類別。
 * 這個類別包含了所有與主要資料庫配置相關的常數定義,包括資料來源類型、套件路徑、Bean名稱等。
 * 
 * 主要功能包括:
 * <ul>
 *   <li>定義資料來源類型和相關套件路徑</li>
 *   <li>定義各種Spring Bean的名稱常數</li>
 *   <li>定義MyBatis相關的設定常數</li>
 *   <li>定義資料來源(DataSource)的設定參數</li>
 *   <li>定義Hibernate相關的設定常數</li>
 * </ul>
  * 
 * 此類別中的所有常數都是以 "primary" 為基礎,用於識別和配置主要資料庫的相關設定。
 * 所有常數皆為 public static final,確保在整個應用程式中保持一致性。
 * 
 * @author Lewis
 * @version 1.0
 */
class PkgConst{ 


    /** DataSource Type (Primary or secondary etc) */
    public static final String SOURCE_TYPE = "primary";

    /** DataSource enabled or not */
    public static final String DATABASE_ENABLED = SOURCE_TYPE + ".datasource.enabled";

    /** DataBase Package prefix */
    public static final String DATABASE_PACKAGE_PREFIX = "tw.lewishome.webapp.database." ;

    /** DataBase Package */
    public static final String DATABASE_PACKAGE = DATABASE_PACKAGE_PREFIX + SOURCE_TYPE;

    /** DataBase Repository 的 Package */
    public static final String REPOSITORY_PACKAGE = DATABASE_PACKAGE + ".repository";

    /** DataBse Entity 的 Package */
    public static final String ENTITY_PACKAGE = DATABASE_PACKAGE + ".entity";

    /** Datasource Bean Name */
    public static final String DATASOURCE_BEAN_NAME = SOURCE_TYPE + "DataSource";

    /** EntityManagerFactory Bean name */
    public static final String ENTITY_MANAGER_FACTORY_BEAN_NAME = SOURCE_TYPE + "EntityManagerFactory";

    /** EntityManager Bean name */
    public static final String ENTITY_MANAGER_BEAN_NAME = SOURCE_TYPE + "EntityManager";

    /** TransactionManager reference bean name */
    public static final String TRANSACTION_MANAGER_REF = SOURCE_TYPE + "TransactionManager";

    /** MyBatis SqlSessionFactory Bean name */
    public static final String SQL_SESSION_FACTOR_BEAN = SOURCE_TYPE + "SqlSessionFactoryBean";

    /** MyBatis SessionTemplate Bean name */
    public static final String SQL_SESSION_TEMPLATE = SOURCE_TYPE + "SessionTemplate";

    /** myBatis Mapper scan package */
    public static final String MYBATIS_SCAN_PACKAGE = DATABASE_PACKAGE + ".mybatis";

    /** DataSource 設定參數 JNDI */
    public static final String DATASOURCE_JNDI = SOURCE_TYPE + ".datasource.jndi";
    // DATABASE_TYPE + ".jndi";

    /** DataSource 設定參數 Driver */
    public static final String DATASOURCE_DRIVER = SOURCE_TYPE + ".datasource.driverClassName";

    /** DataSource 設定參數 URL */
    public static final String DATASOURCE_URL = SOURCE_TYPE + ".datasource.jdbcurl";

    /** DataSource 設定參數 UserName */
    public static final String DATASOURCE_USER_NAME = SOURCE_TYPE + ".datasource.username";

    /** DataSource 設定參數 Password */
    public static final String DATASOURCE_USER_PASSWORD = SOURCE_TYPE + ".datasource.password";

    /** DataSource 設定參數 Persistence Unit Name */
    public static final String PERSISTENCE_UNIT_NAME = SOURCE_TYPE + "PersistenceUnit";

    /** DataSource 設定參數 Hikari DataSource Pool Name */
    public static final String HIKARI_POOL_NAME = SOURCE_TYPE + "HikariPool";

    /** DataSource 設定參數 Hibernate hbm2ddl.auto */
    public static final String HIBERNATE_HBM2DDL_AUTO = SOURCE_TYPE + ".datasource.hibernate.hbm2ddl.auto";

    /** DataSource 設定參數 Hibernate hbm2ddl.import_files */
    public static final String HIBERNATE_IMPORT_FILE = SOURCE_TYPE + ".datasource.hibernate.hbm2ddl.import_files";

    /** DataSource 設定參數 Hibernate dialect */
    public static final String HIBERNATE_DIALECT = SOURCE_TYPE + ".datasource.hibernate.dialect";
   
}

3 新增Primary資料庫設定程式 (PrimaryDataSourceConfig.java)

於 tw.lewishome.webapp.database.primary.configuration內,新增一個Class: PrimaryDataSourceConfig 主要是利用Package-info內的靜態變數,建置primary資料庫相關設定。
https://ithelp.ithome.com.tw/upload/images/20251114/20139477AITJcDYfn6.png

  • 以後不再貼這類新增Class/Package/File等圖片了。
package tw.lewishome.webapp.database.primary.configuration;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.naming.NamingException;
import javax.sql.DataSource;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.TypeHandler;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jndi.JndiTemplate;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.zaxxer.hikari.HikariDataSource;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import tw.lewishome.webapp.base.utility.common.FileUtils;
import tw.lewishome.webapp.base.utility.common.SystemEnvReader;
// import tw.lewishome.webapp.database.audit.AuditFieldInterceptor;
import tw.lewishome.webapp.database.audit.AuditInterceptor;

/**
 * PrimaryDataSourceConfig 是一個 Spring 配置類,用於設置主要數據源的配置。
 * 
 * 此類負責配置 JPA 和 MyBatis 的數據源、實體管理器和事務管理器。
 * 
 * 主要功能包括:
 * <ul>
 * <li>根據不同的環境配置數據源(開發環境和非開發環境)</li>
 * <li>設置 Hibernate 的實體管理器工廠</li>
 * <li>配置事務管理器以支持 JPA 事務</li>
 * <li>設置 MyBatis 的 SqlSessionFactory 和 SqlSessionTemplate</li>
 * </ul>
 * 
 * <h2>使用注意事項:</h2>
 * <ul>
 * <li>JNDI 設定優先於 properties 設定,若 JNDI 未設定則使用 properties。</li>
 * <li>若需自訂連線池參數,可參考註解區塊啟用 HikariCP 設定。</li>
 * <li>MyBatis Mapper XML 檔案需放置於 classpath:/mybatis/primary/ 目錄下。</li>
 * <li>請確認 Package-info 常數類中的相關路徑設定是否正確。</li>
 * </ul>
 * 
 * 數據源的配置根據環境變量進行調整,開發環境使用 HikariCP 進行數據源配置,而非開發環境則使用 JNDI。
 * 
 * @author Lewis
 * @version 1.0
 */
@Configuration
// 起動 Transaction管理功能
@EnableTransactionManagement
// 指定資料庫 Table的 JPARepository 存放 Package(位置)
@EnableJpaRepositories(basePackages = PkgConst.REPOSITORY_PACKAGE, entityManagerFactoryRef = PkgConst.ENTITY_MANAGER_FACTORY_BEAN_NAME, transactionManagerRef = PkgConst.TRANSACTION_MANAGER_REF)
@MapperScan(basePackages = {
        PkgConst.MYBATIS_SCAN_PACKAGE }, sqlSessionFactoryRef = PkgConst.SQL_SESSION_FACTOR_BEAN)
@ConditionalOnProperty(name = PkgConst.DATABASE_ENABLED, havingValue = "true")
public class PrimaryDataSourceConfig {

    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * 
     * Constructs a new PrimaryDataSourceConfig instance.
     * This is the default constructor, implicitly provided by the compiler
     * if no other constructors are defined.
     */
    public PrimaryDataSourceConfig() {
        // Constructor body (can be empty)
    }

    @Autowired
    SystemEnvReader systemEnvReader;

    /**
     * 建立主要資料庫連線的資料來源
     * 
     * 此方法用於非開發環境下,透過JNDI取得資料庫連線
     * 
     * @return 從JNDI取得的DataSource物件,若發生錯誤則返回null
     * @throws NamingException 當JNDI查找失敗時拋出
     * @see PkgConst#DATASOURCE_JNDI
     * @see PkgConst#DATASOURCE_BEAN_NAME
     */
    @Bean(name = PkgConst.DATASOURCE_BEAN_NAME)
    @Primary // spring boot 使用多個資料庫連線時,其中一個必須指定為 @Primary (主要)
    @Profile("!dev")
    public DataSource dataSourceJndi() throws NamingException {
        String datasource_JNDI = systemEnvReader.getProperty(PkgConst.DATASOURCE_JNDI);
        DataSource jndiDataSource = (DataSource) new JndiTemplate().lookup(datasource_JNDI);
        return jndiDataSource;

    }

    /**
     * 建立開發環境的資料庫連線資料來源設定。
     * 
     * 此方法使用 HikariCP 連接池來建立並配置資料庫連線。
     * 被標註為 @Primary,表示在有多個資料庫連線時,這個是主要的連線。
     * 只在開發環境("dev" profile)中啟用。
     *
     * @return 已配置的 HikariDataSource 資料來源
     *         包含以下特定設定:
     *         - 從系統環境讀取資料庫驅動程式、URL、使用者名稱和密碼
     *         - 啟用預處理語句快取
     *         - 設定預處理語句快取大小為 25000
     *         - 設定 SQL 限制大小為 20048
     *         - 使用伺服器端預處理語句
     *         - 設定快速初始化失敗
     *         - 最大連接池大小為 50
     *         - 自動提交設為 false(因應 Postgres 大型物件的需求)
     */
    @Bean(name = PkgConst.DATASOURCE_BEAN_NAME)
    @Primary // spring boot 使用多個資料庫連線時,其中一個必須指定為 @Primary (主要)
    @Profile("dev")
    public DataSource dataSourceHikari() {
        String datasource_Driver = systemEnvReader.getProperty(PkgConst.DATASOURCE_DRIVER);
        String datasource_URL = systemEnvReader.getProperty(PkgConst.DATASOURCE_URL);
        String datasource_UserName = systemEnvReader.getProperty(PkgConst.DATASOURCE_USER_NAME);
        String datasource_Password = systemEnvReader
                .getProperty(PkgConst.DATASOURCE_USER_PASSWORD);
        // spring boot JPA data 自帶的 contention pool (HikariDataSource)
        // 指定 來自 properties內指定的資料庫連線必要參數
        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setDriverClassName(datasource_Driver);
        hikariDataSource.setJdbcUrl(datasource_URL);
        hikariDataSource.setUsername(datasource_UserName);
        hikariDataSource.setPassword(datasource_Password);
        /**
         * HikariCP specific properties. Remove if you move to other connection pooling
         * library.
         **/
        hikariDataSource.addDataSourceProperty("cachePrepStmts", true);
        hikariDataSource.addDataSourceProperty("prepStmtCacheSize", 25000);
        hikariDataSource.addDataSourceProperty("prepStmtCacheSqlLimit", 20048);
        hikariDataSource.addDataSourceProperty("useServerPrepStmts", true);
        hikariDataSource.addDataSourceProperty("initializationFailFast", true);
        hikariDataSource.setMaximumPoolSize(50);
        hikariDataSource.setPoolName(PkgConst.HIKARI_POOL_NAME);
        // // Postgres 大型物件無法被使用在自動確認事物交易模式
        hikariDataSource.setAutoCommit(false);
        return hikariDataSource;
    }

    /**
     * 建立並配置 Primary 資料庫的 EntityManager Bean
     * 
     * @param builder EntityManagerFactoryBuilder 用於建立 EntityManagerFactory
     * @return 配置完成的 EntityManager 實例,如果建立失敗則返回 null
     * 
     * @throws IllegalStateException 當 EntityManagerFactory 建立失敗時可能拋出此異常
     * @throws NamingException       NamingException 當 JNDI 查找失敗時拋出
     */
    @Bean(name = PkgConst.ENTITY_MANAGER_BEAN_NAME)
    @Primary // spring boot 使用多個資料庫連線時,其中一個必須指定為 @Primary (主要)
    public EntityManager entityManager(EntityManagerFactoryBuilder builder)
            throws IllegalStateException, NamingException {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = entityManagerFactory(builder);
        EntityManagerFactory entityManagerFactory = entityManagerFactoryBean.getObject();
        if (entityManagerFactory == null) {
            System.out.println("Failed to Create EntityManager");
            return null;
        } else {
            return entityManagerFactory.createEntityManager();
        }
    }

    /**
     * 建立並配置 Primary 資料庫的實體管理器工廠 (EntityManagerFactory)。
     * 
     * 此方法會根據系統環境變數設定 Hibernate 的相關屬性,並使用指定的資料來源來建立實體管理器工廠。
     * 
     * @param builder EntityManagerFactoryBuilder 用於建立 EntityManagerFactory
     * @return 配置完成的 LocalContainerEntityManagerFactoryBean 實例
     * @throws NamingException
     */
    @Bean(name = PkgConst.ENTITY_MANAGER_FACTORY_BEAN_NAME)
    @Primary // spring boot 使用多個資料庫連線時,其中一個必須指定為 @Primary (主要)
    LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder)
            throws NamingException {
        String hbm2ddl_auto = systemEnvReader.getProperty(PkgConst.HIBERNATE_HBM2DDL_AUTO);
        String hbm2ddl_import_files = systemEnvReader
                .getProperty(PkgConst.HIBERNATE_IMPORT_FILE);
        String hibernate_dialect = systemEnvReader.getProperty(PkgConst.HIBERNATE_DIALECT);

        HashMap<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", hbm2ddl_auto);
        // 正常不太需要import data , 若有指定而檔案不存在,啟動時會Error
        if (hbm2ddl_import_files != null) {
            properties.put("hibernate.hbm2ddl.import_files", hbm2ddl_import_files);
        }
        if (hibernate_dialect != null) {
            properties.put("hibernate.dialect", hibernate_dialect);
        }

        DataSource datasource = null;
        String activeEnv = systemEnvReader.getProperty("spring.profiles.active");
        if (StringUtils.containsAnyIgnoreCase(activeEnv, "dev")) {
            datasource = this.dataSourceJndi();
        } else {
            datasource = this.dataSourceHikari();
        }
        return builder.dataSource(datasource).properties(properties)
                .packages(PkgConst.ENTITY_PACKAGE)
                .persistenceUnit(PkgConst.PERSISTENCE_UNIT_NAME)
                .build();
    }

    /**
     * 建立並設定交易管理器 (Transaction Manager)。
     * 當使用多個資料庫連線時,可以透過此Bean管理資料庫交易。
     *
     * @param localContainerEntityManagerFactoryBean 實體管理器工廠Bean,透過@Qualifier指定特定的資料來源
     * @return 若成功建立實體管理器工廠則返回JpaTransactionManager實例,否則返回null
     */
    @Bean(name = PkgConst.TRANSACTION_MANAGER_REF)
    PlatformTransactionManager transactionManager(
            // 使用多個資料庫連線時,需要指定唯一的@Qualifier名稱
            final @Qualifier(PkgConst.ENTITY_MANAGER_FACTORY_BEAN_NAME) LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean) {
        EntityManagerFactory entityManagerFactory = localContainerEntityManagerFactoryBean.getObject();
        if (entityManagerFactory != null) {
            return new JpaTransactionManager(entityManagerFactory);
        } else {
            return null;
        }
    }

    // https://www.tpisoftware.com/tpu/articleDetails/2158

    /**
     * 建立並回傳 MyBatis 的 SqlSessionFactory 實例,供 primary 資料來源使用。
     *
     * <p>
     * 此方法會:
     * <ul>
     * <li>使用傳入的 DataSource 作為資料來源。</li>
     * <li>設定實體別名的掃描路徑(typeAliasesPackage),以便 MyBatis 可將實體類別當作別名使用。</li>
     * <li>載入 mapper XML 檔案的位置(mapperLocations),以初始化 SQL 映射定義。</li>
     * </ul>
     *
     * 此方法會在 Spring 容器中註冊為一個 Bean,其名稱由
     * PrimaryDataBaseConstants.SQL_SESSION_FACTOR_BEAN 指定。
     *
     * @param dataSource 已以 @Qualifier(PrimaryDataBaseConstants.DATASOURCE_BEAN_NAME)
     *                   指定的資料來源,用於建立 SqlSessionFactory
     * @return 已初始化的 SqlSessionFactory,可用來建立 SqlSession 並執行 MyBatis 的資料存取操作
     * @throws Exception 如果在建立或初始化 SqlSessionFactoryBean 時發生錯誤(例如載入 mapper
     *                   檔案失敗或設定錯誤)
     */

    @Bean(name = PkgConst.SQL_SESSION_FACTOR_BEAN)
    SqlSessionFactoryBean sqlSessionFactory(@Qualifier(PkgConst.DATASOURCE_BEAN_NAME) DataSource dataSource)
            throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);

        // register TypeHandlers 
        sqlSessionFactoryBean.setTypeHandlers(arrayTypeHandlers());

        sqlSessionFactoryBean.setTypeAliasesPackage(PkgConst.ENTITY_PACKAGE);
        

        // audit interceptor add Entity Audit Fields       
        sqlSessionFactoryBean.setPlugins(new AuditInterceptor());

        // set multi mapper XML location 
        sqlSessionFactoryBean.setMapperLocations(resourceLocations());

        // // for single mapper location       
        // sqlSessionFactoryBean.setMapperLocations(
        //         new PathMatchingResourcePatternResolver().getResources("classpath:/mybatis/primary/*.xml"));

        return sqlSessionFactoryBean;
    }

    /**
     * 建立並回傳 SqlSessionTemplate 供 MyBatis 執行 SQL 操作。
     *
     * 這個方法會使用注入的 SqlSessionFactory 建構 SqlSessionTemplate,並註冊為 Spring Bean。
     * Bean 名稱使用 PrimaryDataBaseConstants.SQL_SESSION_TEMPLATE 常數;注入的
     * SqlSessionFactory
     * 由 PrimaryDataBaseConstants.SQL_SESSION_FACTOR_BEAN 所指定的 Qualifier
     * 提供,代表主要資料來源的
     * MyBatis 設定。
     *
     * @param sqlSessionFactory 使用
     *                          {@code @Qualifier(PrimaryDataBaseConstants.SQL_SESSION_FACTOR_BEAN)}
     *                          注入的 SqlSessionFactory,
     *                          用於建立 SqlSessionTemplate。
     * @return 已建立的 {@link org.mybatis.spring.SqlSessionTemplate} 實例,可用於執行 MyBatis
     *         的查詢、更新與映射操作。
     * @throws Exception 若在建立 SqlSessionTemplate 過程中發生錯誤(例如 SqlSessionFactory
     *                   尚未正確初始化),則拋出例外。
     */
    @Bean(name = PkgConst.SQL_SESSION_TEMPLATE)
    SqlSessionTemplate sessionTemplate(
            @Qualifier(PkgConst.SQL_SESSION_FACTOR_BEAN) SqlSessionFactoryBean sqlSessionFactoryBean)
            throws Exception {
        return new SqlSessionTemplate(sqlSessionFactoryBean.getObject());
    }

    // for multiple mapper locations
    private Resource[] resourceLocations() {
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        List<String> mapperLocations = new ArrayList<>();
        mapperLocations.add("classpath:/mybatis/*.xml");
        mapperLocations.add("classpath:/mybatis/" + PkgConst.SOURCE_TYPE + "/*.xml");
        // mapperLocations.add("classpath:/mybatis/secondary/*.xml");
        // mapperLocations.add("classpath:/mybatis/third/*.xml");
        // mapperLocations.add("classpath:/mybatis/fourth/*.xml");
        List<Resource> resources = new ArrayList<>();
        if (!mapperLocations.isEmpty()) {
            for (String mapperLocation : mapperLocations) {
                try {
                    Resource[] mappers = resourcePatternResolver.getResources(mapperLocation);
                    resources.addAll(List.of(mappers));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return resources.toArray(Resource[]::new);
    }

    /** find all DataKeyHandler classes and instantiate them as TypeHandlers */
    private TypeHandler<?>[] arrayTypeHandlers() {
        List<TypeHandler<?>> listTypeHandler = new ArrayList<>();
        try {
            List<Class<?>> listEntityClasses = FileUtils.getClassesInPackage(PkgConst.ENTITY_PACKAGE);
            System.out.println("Entity Classes Found: " + listEntityClasses.size());
            List<String> listDataKeyHandlerClassNames = new ArrayList<>();
            for (Class<?> entityClass : listEntityClasses) {    
                if (entityClass.getName().contains("$DataKeyHandler")) {
                    listDataKeyHandlerClassNames.add(entityClass.getName());                
                }
            }
            for (String dataKeyHandlerClassNames : listDataKeyHandlerClassNames) {    
                try {
                    Class<?> typeHandlerClass = Class.forName(dataKeyHandlerClassNames);
                    if (TypeHandler.class.isAssignableFrom(typeHandlerClass)) {
                        TypeHandler<?> typeHandlerInstance = (TypeHandler<?>) typeHandlerClass.getDeclaredConstructor().newInstance();
                        listTypeHandler.add(typeHandlerInstance);
                        System.out.println("Added TypeHandler: " + dataKeyHandlerClassNames);
                    }
                } catch (Exception e) {
                    continue;
                }
            }
        } catch (ClassNotFoundException | IOException e) {
            return listTypeHandler.toArray(TypeHandler<?>[]::new);
        }

        return listTypeHandler.toArray(TypeHandler<?>[]::new);
    }
}

建置Secondary Datasource (資料庫)設定

1. application-dev.properties相關新增與調整

於設置 Primary資料庫時,已經有加入Secondary相關設定,與Primary不同的是多了 secondary.datasource.enabled=true 來確認專案是否需要啟動 Secondary資料庫(開關)

#Secondary Datasource 是否有需要
secondary.datasource.enabled=true
secondary.datasource.jdbcurl=jdbc:oracle:thin:@oracle.lewishome.tw:1521/orclpdb.lewishome.tw
secondary.datasource.username=OUSERXX1
# Database connection password 建議不要存在此駔,使用 Keystore(安全線以及後續密碼交出去給資管理部)
secondary.datasource.password=XXXXX
secondary.datasource.driverClassName=oracle.jdbc.OracleDriver
secondary.datasource.hibernate.hbm2ddl.auto=update

2. 新增Secondary資料庫常數設定File (package-info.java)

於 tw.lewishome.webapp.database.secondary.configuration內,新增 File: package-info.java,主要是用於secondary資料庫常數設定,以及關於此Package的 Javadoc說明,程式碼部分,因為已經有命名規則來架構化的,所以拿 primary 的package-info.java內容(拷貝),之須將 "primary" 調整為 "secondary"

包含JavaDoc註解說明以及程式碼需要調整應該不多 (以下3個primary 改為 secondary)。

  • package tw.lewishome.webapp.database.primary.configuration;
  • 此類別中的所有常數都是以 "primary" 為基礎,用於識別和配置主要資料庫的相關設定。
  • public static final String SOURCE_TYPE = "primary";

2 新增Secondary資料庫設定程式 (SecondaryDataSourceConfig.java)

於 tw.lewishome.webapp.database.secondary.configuration內,新增一個Class: SecondaryDataSourceConfig 主要是利用Package-info內的靜態變數,建置secondary資料庫相關設定,因為已經有命名規則來架構化的,所以拿 primary 的 PrimaryDataSourceConfig.java內容(拷貝),之須將 "primary" 調整為 "secondary" 。

  1. (按Ctl-F) 收尋功能,替換 "primary" 字串為 "secondary"
    https://ithelp.ithome.com.tw/upload/images/20251115/201394777VfSLplXNI.png
  • 區分大寫改一次 (Primary -> Secondary ) 應該21處
    https://ithelp.ithome.com.tw/upload/images/20251115/2013947789zeqCMdrb.png

  • 區分小寫改一次 (primary -> secondary ) 應該有4處
    https://ithelp.ithome.com.tw/upload/images/20251115/20139477ta1dfUipy3.png

2 刪除替換之後的錯誤程式碼 (因為 @Primary 變 @Secondary)

但沒有此Annotation

  • 刪除 import org.springframework.context.annotation.Secondary;

沒有此@Secondary ,共4處

  • 刪除 @Secondary // spring boot 使用多個資料庫連線時,其中一個必須指定為 @Secondary (主要)

以上完成配置多資料庫支援

調整 application-dev.properties 的參數,啟動Spring Boot

  • secondary.datasource.enabled=false
  • 只會有一個 Primary DataBase設置,mybatis的錯誤,暫時不理會。
    https://ithelp.ithome.com.tw/upload/images/20251115/20139477E523VpCdeK.png
  • secondary.datasource.enabled=true
  • 會有兩個 Primary & secondary DataBase設置,同樣有兩個 mybatis的錯誤。
    https://ithelp.ithome.com.tw/upload/images/20251115/20139477GqBI2t4L1A.png

後續第三個或在多個,都可以比照Secondary來增加。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言