通常大多數專案,配置一個專用的資料庫即可運作,簡單設定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專案轉換。
集中資料庫相關設定與物件,放置 專案跟目錄下的 database 目錄 (tw.lewishome.webapp.database)
所有Database共用的 Audit物件
依database 分類(primary、secondary、tertiary)區分不同資料庫連線
每個資料庫連線各功能物件區分(以 primary 為例)
*檢核建置完成的database專案目錄:
tw.lewishome.webapp.database.audit Pakcage內新增AuditInterceptor.java,內容專為Mybatis 新增或更改資料時,自動完成Auidt欄位
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
}
}
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的啟動設計:
*注意本專案的資料庫連線參數有設計名稱規則,以 "primary"或 "secondary" 前置區分資料庫的url、user、password等其他連線參數名稱。但使用JNDI則依系統管理規則,沒有限制。
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
#各類資料庫的 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
#Store primary Datasource (這些是自訂的變數名稱,只要與程式內取用的設定一致即可)
#primary Datasource (using JNDI)
primary.datasource.jndi=java:jboss/datasources/sqlserverDS
primary.datasource.hibernate.hbm2ddl.auto=update
於 tw.lewishome.webapp.database.primary.configuration內,新增 File: package-info.java,主要是用於primary資料庫常數設定,以及關於此Package的 Javadoc說明,
a. Vscode 新增File(若選新增Class,會因檔名有 '-' 所以會錯誤無法新增)
b. 輸入File名稱(.java不能省略)
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";
}
於 tw.lewishome.webapp.database.primary.configuration內,新增一個Class: PrimaryDataSourceConfig 主要是利用Package-info內的靜態變數,建置primary資料庫相關設定。
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);
}
}
於設置 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
於 tw.lewishome.webapp.database.secondary.configuration內,新增 File: package-info.java,主要是用於secondary資料庫常數設定,以及關於此Package的 Javadoc說明,程式碼部分,因為已經有命名規則來架構化的,所以拿 primary 的package-info.java內容(拷貝),之須將 "primary" 調整為 "secondary"
包含JavaDoc註解說明以及程式碼需要調整應該不多 (以下3個primary 改為 secondary)。
於 tw.lewishome.webapp.database.secondary.configuration內,新增一個Class: SecondaryDataSourceConfig 主要是利用Package-info內的靜態變數,建置secondary資料庫相關設定,因為已經有命名規則來架構化的,所以拿 primary 的 PrimaryDataSourceConfig.java內容(拷貝),之須將 "primary" 調整為 "secondary" 。
區分大寫改一次 (Primary -> Secondary ) 應該21處
區分小寫改一次 (primary -> secondary ) 應該有4處
2 刪除替換之後的錯誤程式碼 (因為 @Primary 變 @Secondary)
但沒有此Annotation
沒有此@Secondary ,共4處