服務(wù)從mysql遷移達(dá)夢(mèng)數(shù)據(jù)庫(kù)的實(shí)現(xiàn)方案
一、背景
根據(jù)信創(chuàng)國(guó)產(chǎn)適配要求,針對(duì)業(yè)務(wù)系統(tǒng)遷移至達(dá)夢(mèng)數(shù)據(jù)庫(kù),保持系統(tǒng)穩(wěn)定、可靠。 當(dāng)前系統(tǒng)技術(shù)棧:springboot+mybatis(3.5.9)+mybaits plus 我們采用的是攔截器方案,結(jié)合JavaAgent字節(jié)碼增強(qiáng)技術(shù),實(shí)現(xiàn)最小化代碼改動(dòng)。
二、遷移方案
- 通過(guò)攔截器實(shí)現(xiàn)
- 通過(guò)mybatis自帶的databaseId 多數(shù)據(jù)源機(jī)制
三、梳理現(xiàn)有使用方式
| 使用方式 | demoMapperXml | demoMapperAnno | demoMapper | demoBeanService | queryWrap |
|---|
四、方案實(shí)現(xiàn)
4.1 攔截器方式實(shí)現(xiàn)
主要實(shí)現(xiàn)兩個(gè)攔截器:
攔截器 作用 備注 mapper調(diào)用方法級(jí)別攔截器 攔截復(fù)雜sql的執(zhí)行方法 精確到單個(gè)方法的攔截,僅對(duì)達(dá)夢(mèng)不支持的方法進(jìn)行更改 sql函數(shù)攔截器 簡(jiǎn)單的函數(shù)替換例如(?group_concat->WM_CONCA )
4.1.1 方法攔截器
實(shí)現(xiàn)原理:
在MyBatis調(diào)用Mapper方法時(shí)進(jìn)行攔截 自動(dòng)將原方法調(diào)用重定向到對(duì)應(yīng)的DM版本方法 示例:
// 原方法
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(@Param("id") Long id);
// DM專用方法(僅當(dāng)原SQL不兼容時(shí)需實(shí)現(xiàn))
@Select("SELECT * FROM user WHERE id = #{id}") // 修改后的DM兼容SQL
User selectByIdDM(@Param("id") Long id);
攔截邏輯:
檢查是否存在原方法名+DM后綴的方法 存在則調(diào)用DM版本方法,否則調(diào)用原方法
4.1.2 sql函數(shù)攔截器
對(duì)sql的進(jìn)行攔截并處理,處理簡(jiǎn)單函數(shù)轉(zhuǎn)換
例如:
group_concat → WM_CONCAT 遷移SQL分類(lèi)處理
| 類(lèi)別 | 處理方式 | 描述 | 示例 |
|---|---|---|---|
| 無(wú)需更改 | 直接執(zhí)行 | DM 數(shù)據(jù)庫(kù)原生兼容的 SQL 語(yǔ)法,無(wú)需任何轉(zhuǎn)換。 | SELECT * FROM employees WHERE id = 1; |
| 僅替換函數(shù) | 函數(shù)攔截器處理 | 僅需將源數(shù)據(jù)庫(kù)特有的函數(shù)名一對(duì)一映射為 DM 的等效函數(shù)。 | GROUP_CONCAT(name) → WM_CONCAT(name) |
| 需要重寫(xiě)SQL | 方法攔截器 + DM 方法 | SQL 的語(yǔ)義或語(yǔ)法結(jié)構(gòu)不同,需用 DM 的特定語(yǔ)法重寫(xiě)整條語(yǔ)句。 | REPLACE INTO table ... → MERGE INTO table ... |
| 重寫(xiě)SQL + 替換函數(shù) | 方法攔截器 + DM 方法 | 復(fù)雜的 SQL 語(yǔ)句,其結(jié)構(gòu)需要重寫(xiě),并且其中包含需要替換的特殊函數(shù)。 | 包含 GROUP_CONCAT 和 LIMIT 分頁(yè)的復(fù)雜查詢,需重寫(xiě)為使用 WM_CONCAT 和 ROWNUM 的 DM 語(yǔ)法。 |

代碼整體情況
方法攔截器代碼
import cn.hutool.json.JSONUtil;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
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.plugin.*;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
// 定義攔截器簽名
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}),
@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})
})
//@Component
@Slf4j
public class MethodInterceptor implements Interceptor {
@Value("${task.dbType:mysql}")
private String dbType; // 數(shù)據(jù)庫(kù)類(lèi)型配置
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this); // 包裝目標(biāo)對(duì)象
}
@Override
public void setProperties(Properties properties) {} // 設(shè)置屬性方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("intercept-開(kāi)始執(zhí)行方法-" + invocation.getMethod().getName());
// 獲取StatementHandler
Object[] args = invocation.getArgs(); // 方法參數(shù)
MappedStatement ms = (MappedStatement) args[0]; // 映射語(yǔ)句
String methodId = ms.getId(); // 方法ID
RowBounds rowBounds = (RowBounds)args[2];
// 如果是DM數(shù)據(jù)庫(kù)且方法名不帶Dm后綴
if ("dm".equals(dbType) && !methodId.endsWith("DM")) {
try {
// 嘗試查找對(duì)應(yīng)的Dm方法
Configuration configuration = ms.getConfiguration();
// 處理 namespace 邏輯
String fullMethodId = methodId;
String noFullMethodId = methodId;
if (!methodId.contains(".")) {
// 如果不包含點(diǎn)(.),說(shuō)明是簡(jiǎn)短的方法ID,需要加上namespace
String namespace = ms.getResource().replace(".", "/"); // 獲取namespace
fullMethodId = namespace + "." + methodId;
}else{
noFullMethodId = methodId.substring(methodId.lastIndexOf(".") + 1);
}
String dmFullMethodId = fullMethodId + "DM";
String dmNoFullMethodId = noFullMethodId + "DM";
String dmMethodId = dmFullMethodId;
MappedStatement dmMs = getDmMappedStatement(configuration, dmFullMethodId);
if(dmMs == null){
dmMethodId = dmNoFullMethodId;
dmMs = getDmMappedStatement(configuration, dmNoFullMethodId);
}
Object parameter = args[1];
// 如果找到Dm方法則替換執(zhí)行
if (dmMs != null) {
BoundSql newBoundSql = dmMs.getBoundSql(parameter);
log.info("intercept執(zhí)行的方法是-" + dmMethodId);
args[0] = dmMs; // 替換為DM方法
if (args.length == 4) {
args[2] = newBoundSql;
} else {
args[5] = newBoundSql;
}
}
} catch (Exception e) {
log.error("intercept-沒(méi)有找到對(duì)應(yīng)的Dm方法-繼續(xù)執(zhí)行原方法-或嘗試使用sql替換-" + methodId,e);
}
}
Object proceed = invocation.proceed(); // 執(zhí)行攔截方法
log.info(JSONUtil.parse(proceed).toStringPretty()); // 記錄執(zhí)行結(jié)果
return proceed;
}
public MappedStatement getDmMappedStatement(Configuration configuration, String methodId) {
return configuration.getMappedStatement(methodId);
}
}
sql攔截器代碼
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})
})
@Log
public class ReWriteInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 獲取 StatementHandler
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String originalSql = boundSql.getSql();
log.info("OriginalSql: " + originalSql.replaceAll("\n"," "));
String rewrittenSql = rewriteSql(originalSql);
// 用反射重新設(shè)置 SQL
Field sqlField = boundSql.getClass().getDeclaredField("sql");
sqlField.setAccessible(true);
sqlField.set(boundSql, rewrittenSql);
Object proceed = invocation.proceed();
return proceed;
}
private String rewriteSql(String originalSql) {
// mysql IFNULL(col1,col2) => NVL(col1,col2) dm
originalSql = originalSql.replaceAll("(?i)GROUP_CONCAT", "WM_CONCAT");
originalSql = originalSql.replaceAll("(?i)!JSON_CONTAINS", " not JSON_CONTAINS ");
originalSql = originalSql.replaceAll("(?i)INSERT\\s+IGNORE\\s+INTO", "INSERT INTO ");
originalSql = convertCastToToChar(originalSql);
log.info("OriginalSql: " + originalSql+"\nRewrittenSql: " + originalSql);
return originalSql;
}
/**
* 將cast(... as char)轉(zhuǎn)換為to_char(...)格式
* @param sql
* @return {@link String}
*/
public static String convertCastToToChar(String sql) {
// 正則表達(dá)式匹配CAST(... AS CHAR)模式
Pattern pattern = Pattern.compile(
"CONVERT\\s*\\(\\s*(.+?)\\s*,\\s*(?:CHAR|VARCHAR|VARCHAR2|TEXT)\\s*(?:\\(\\d+\\))?\\s*\\)",
Pattern.CASE_INSENSITIVE
);
Matcher matcher = pattern.matcher(sql);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
// 獲取CAST內(nèi)部的表達(dá)式
String innerExpression = matcher.group(1).trim();
// 替換為T(mén)O_CHAR格式
matcher.appendReplacement(result, "TO_CHAR(" + innerExpression + ")");
}
matcher.appendTail(result);
return result.toString();
}
mybatis2dmAutoConfiguration 代碼
@Configuration
@ConditionalOnExpression("'${spring.datasource.url}'.startsWith('jdbc:dm:')")//必須是達(dá)夢(mèng)的驅(qū)動(dòng)才生效
public class mybatis2dmAutoConfiguration {
@Bean
public DmMethodRouterInterceptor dmMethodRouterInterceptor() {
return new DmMethodRouterInterceptor();
}
@Bean
public ReWriteInterceptor reWriteInterceptor() {
return new ReWriteInterceptor();
}
@Bean
public ConfigurationCustomizer mybatisConfigurationCustomizer() {
return configuration -> {
configuration.addInterceptor(new DmMethodRouterInterceptor());
configuration.addInterceptor(new ReWriteInterceptor());
};
}
}
4.2 通過(guò)mybatis自帶的databaseId 多數(shù)據(jù)源機(jī)制
話不多說(shuō),直接上代碼。
MyBatisConfig 代碼
@Configuration
public class MyBatisConfig {
@Bean
public DatabaseIdProvider databaseIdProvider() {
VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
Properties properties = new Properties();
properties.setProperty("MySQL", "mysql");
properties.setProperty("DM DBMS", "dm");
databaseIdProvider.setProperties(properties);
return databaseIdProvider;
}
}
xml代碼
<!-- 通用SQL(所有數(shù)據(jù)庫(kù)都會(huì)嘗試使用) --> <select id="selectUser" resultType="User"> SELECT * FROM user </select> <!-- MySQL專用SQL(僅MySQL使用) --> <select id="selectUser" resultType="User" databaseId="mysql"> SELECT * FROM user LIMIT 1 </select> <!-- DM專用SQL(僅達(dá)夢(mèng)使用) --> <select id="selectUser" resultType="User" databaseId="dm"> SELECT TOP 1 * FROM user </select>
在運(yùn)行代碼過(guò)程中,會(huì)根據(jù)databaseId來(lái)選擇數(shù)據(jù)源運(yùn)行對(duì)象的sql。
方案對(duì)比
| 方案特性 | 攔截器方式 | 原生 MyBatis DatabaseId |
|---|---|---|
| 實(shí)現(xiàn)原理 | 通過(guò)自定義 MyBatis 攔截器(及JavaAgent字節(jié)碼增強(qiáng))動(dòng)態(tài)解析和重寫(xiě)SQL | 依賴 MyBatis 內(nèi)置的 <databaseIdProvider> 和 XML 中 databaseId 屬性 |
| 支持的映射方式 | ||
- XML 映射文件 (demoMapperXml) | ? 支持 | ? 支持 |
- 注解方式 (demoMapperAnno) | ? 支持 | ? 不支持 |
- MyBatis-Plus 等擴(kuò)展 (demoMapper) | ? 支持 | ? 不支持 |
- Service/Bean 中的直接SQL (demoBeanService) | ? 支持 | ? 不支持 |
- 查詢包裝器 (queryWrap) | ? 支持 | ? 不支持 |
| 靈活性 | 高:可處理任意位置的SQL,支持復(fù)雜邏輯和函數(shù)替換 | 低:僅限XML中定義的語(yǔ)句,無(wú)法處理運(yùn)行時(shí)生成的SQL |
| 侵入性 | 低:對(duì)業(yè)務(wù)代碼基本無(wú)侵入,通過(guò)配置和字節(jié)碼增強(qiáng)實(shí)現(xiàn) | 中:需要在所有XML語(yǔ)句中顯式定義 databaseId 屬性 |
| 維護(hù)成本 | 集中維護(hù)攔截規(guī)則,一改全改 | 分散維護(hù),每個(gè)XML語(yǔ)句都需要適配不同數(shù)據(jù)庫(kù)的版本 |
實(shí)施步驟
- 準(zhǔn)備工作: 對(duì)SQL及其mapper方法進(jìn)行分類(lèi)標(biāo)注
- 開(kāi)發(fā)攔截器: 實(shí)現(xiàn)方法級(jí)別攔截邏輯 配置函數(shù)替換規(guī)則
- JavaAgent集成: 打包攔截器為JavaAgent 配置JVM啟動(dòng)參數(shù)加載Agent
- 部署上線: 添加JavaAgent到生產(chǎn)環(huán)境啟動(dòng)參數(shù) 代碼在最后面,具體的工程我后續(xù)會(huì)上傳至:
優(yōu)勢(shì)
最小化代碼改動(dòng):僅需為不兼容的方法添加DM版本 非侵入式:通過(guò)字節(jié)碼增強(qiáng)避免直接修改業(yè)務(wù)代碼 靈活可控:可精確控制哪些方法需要特殊處理 易于維護(hù):DM相關(guān)修改集中管理,與主邏輯分離 補(bǔ)充點(diǎn): 事務(wù)都是支持的
最終實(shí)現(xiàn)代碼

整體工程結(jié)構(gòu)
RewriteAgent
public class RewriteAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("mysql2dm agent start");
AgentBuilder agentBuilder = new AgentBuilder.Default()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(AgentBuilder.Listener.StreamWriting.toSystemOut().withTransformationsOnly());
// 攔截 BaseExecutor.query 和 update
agentBuilder = agentBuilder
.type(ElementMatchers.named("org.apache.ibatis.executor.BaseExecutor"))
.transform((builder, typeDescription, classLoader, module) ->
builder
.method(ElementMatchers.named("query").or(ElementMatchers.named("update")))
.intercept(Advice.to(ExecutorAdvice.class))
)
// ?? 注意:下面這個(gè)攔截器是追加,而不是覆蓋上面
.type(ElementMatchers.named("org.apache.ibatis.executor.statement.RoutingStatementHandler"))
.transform((builder, typeDescription, classLoader, module) ->
builder
.method(ElementMatchers.named("prepare"))
.intercept(Advice.to(SqlRewriteAdvice.class))
);
agentBuilder.installOn(inst);
}
}
BoundSqlHolder
public class BoundSqlHolder {
private static final ThreadLocal<BoundSql> holder = new ThreadLocal<>();
public static void setBoundSql(BoundSql boundSql) {
holder.set(boundSql);
}
public static BoundSql getBoundSql() {
return holder.get();
}
public static void clear() {
holder.remove();
}
}
ExecutorAdvice
public class ExecutorAdvice {
@Advice.OnMethodEnter
public static void enter(@Advice.AllArguments Object[] args) {
try {
MappedStatement originalMs = (MappedStatement) args[0];
String methodId = originalMs.getId();
if (methodId != null && !methodId.endsWith("DM")) {
Configuration config = originalMs.getConfiguration();
String dmMethodId = methodId + "DM";
if (config.hasStatement(dmMethodId)) {
MappedStatement dmMs = config.getMappedStatement(dmMethodId);
System.out.println("[ExecutorAdvice] 替換為 DM 方法: " + dmMethodId);
args[0] = dmMs;
Object parameter = args[1];
BoundSql newBoundSql = dmMs.getBoundSql(parameter);
BoundSqlHolder.setBoundSql(newBoundSql);
System.out.println("[ExecutorAdvice] 替換后的 SQL: " + newBoundSql.getSql());
}
}
} catch (Exception e) {
System.err.println("[ExecutorAdvice] 替換失敗: " + e.getMessage());
}
}
}
SqlRewriteAdvice
public class SqlRewriteAdvice {
@Advice.OnMethodEnter
public static void enter(@Advice.This Object handlerObj,
@Advice.Origin String method,
@Advice.AllArguments Object[] args) {
try {
StatementHandler handler = (StatementHandler) handlerObj; // 手動(dòng)強(qiáng)轉(zhuǎn)(運(yùn)行時(shí))
Field delegateField = handler.getClass().getDeclaredField("delegate");
delegateField.setAccessible(true);
StatementHandler realHandler = (StatementHandler) delegateField.get(handler);
BoundSql boundSql = handler.getBoundSql();
BoundSql newBoundSql = BoundSqlHolder.getBoundSql();
if (newBoundSql != null) {
String originalSql = newBoundSql.getSql();
System.out.println("Original SQL: " + originalSql);
// 重寫(xiě) SQL
String rewrittenSql = rewriteSql(originalSql);
Field boundSqlField = realHandler.getClass().getSuperclass().getDeclaredField("boundSql");
boundSqlField.setAccessible(true);
boundSqlField.set(realHandler, newBoundSql);
System.out.println("Rewritten SQL: " + rewrittenSql);
}else {
String originalSql = boundSql.getSql();
System.out.println("Original SQL: " + originalSql);
String rewrittenSql = rewriteSql(originalSql);
Field sqlField = boundSql.getClass().getDeclaredField("sql");
sqlField.setAccessible(true);
sqlField.set(boundSql, rewrittenSql);
System.out.println("Rewritten SQL: " + rewrittenSql);
}
} catch (Exception e) {
System.out.println("SQL Rewrite Error: " + e);
}finally {
// 清理,防止內(nèi)存泄漏
BoundSqlHolder.clear();
}
}
public static String rewriteSql(String originalSql) {
originalSql = originalSql.replaceAll("(?i)GROUP_CONCAT", "WM_CONCAT");
originalSql = originalSql.replaceAll("(?i)!JSON_CONTAINS", " not JSON_CONTAINS ");
originalSql = originalSql.replaceAll("(?i)INSERT\\s+IGNORE\\s+INTO", "INSERT INTO ");
originalSql = originalSql.replaceAll("(?i)GROUP_CONCAT", "WM_CONCAT");
return originalSql;
}
在啟動(dòng)方法參數(shù)上添加:
-javaagent:E:\\cbimWorkSpace\\mybatis2dm-agent\\target\\mybatis2dm-agent-1.0-SNAPSHOT.jar
完結(jié),撒花!
總結(jié)
到此這篇關(guān)于服務(wù)從mysql遷移達(dá)夢(mèng)數(shù)據(jù)庫(kù)的文章就介紹到這了,更多相關(guān)mysql遷移達(dá)夢(mèng)數(shù)據(jù)庫(kù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MySQL外鍵類(lèi)型及應(yīng)用場(chǎng)景總結(jié)
這篇文章主要介紹了?MySQL?外鍵的類(lèi)型(RESTRICT、CASCADE、SET?NULL、NO?ACTION)及其應(yīng)用場(chǎng)景、優(yōu)缺點(diǎn)和使用注意事項(xiàng),通過(guò)創(chuàng)建和測(cè)試外鍵,闡述了不同類(lèi)型外鍵在主表刪除或更新數(shù)據(jù)時(shí)子表的變化,需要的朋友可以參考下2024-12-12
MySQL學(xué)習(xí)之DDL數(shù)據(jù)庫(kù)定義與操作
本文詳細(xì)介紹SQL中DDL的數(shù)據(jù)庫(kù)操作,包括查詢、創(chuàng)建、刪除數(shù)據(jù)庫(kù)和表的操作,以及修改表結(jié)構(gòu)等功能,通過(guò)這些操作,讀者可以深入了解如何使用SQL進(jìn)行數(shù)據(jù)庫(kù)管理和維護(hù),需要的朋友可以參考下2024-11-11
mysql注入之長(zhǎng)字符截?cái)?orderby注入,HTTP分割注入,limit注入方式
這篇文章主要介紹了mysql注入之長(zhǎng)字符截?cái)?orderby注入,HTTP分割注入,limit注入方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11
MySQL批量處理圖片URL統(tǒng)一去掉域名前綴的方法
文章介紹了如何使用MySQL 8.0的新函數(shù)REGEXP_REPLACE()批量處理圖片URL,去掉域名前綴,將所有圖片路徑統(tǒng)一存為相對(duì)路徑,從而避免路徑重復(fù)和加載錯(cuò)誤,需要的朋友可以參考下2025-11-11
mysql 搜尋附近N公里內(nèi)數(shù)據(jù)的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇mysql 搜尋附近N公里內(nèi)數(shù)據(jù)的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04

