1. 背景項目中要做國產(chǎn)化,MySQL要替換成達夢8數(shù)據(jù)庫。項目中MySQL的建表語句和內(nèi)置數(shù)據(jù)通過.sql文件維護,安裝時會初始化表結(jié)構(gòu)和表內(nèi)置數(shù)據(jù)。項目架構(gòu)為SpringBoot + JPA / Mybatis。適配工作內(nèi)容包括數(shù)據(jù)庫遷移、數(shù)據(jù)導(dǎo)出、項目中的配置更改和相關(guān)問題解決方案。 2. 數(shù)據(jù)處理流程1. 前期裝備1. 安裝達夢8數(shù)據(jù)庫達夢官網(wǎng)有提供安裝包,根據(jù)自己的場景進行選擇,linux_x86或者linux_aarch64,由于我們項目要全面國產(chǎn)化,所以服務(wù)器用的國產(chǎn)華為的鯤鵬服務(wù)器(aarch64),操作系統(tǒng)為國產(chǎn)銀河麒麟V10。安裝步驟按官網(wǎng)提供的文檔就行,下載后安裝包里也會有一些PDF說明文檔可參考。 2. 創(chuàng)建庫,啟動安裝時如果選擇了圖形化界面安裝,則有 2. 庫數(shù)據(jù)處理這一步的處理主要是將之前項目中存儲的 1.先把之前sql文件(MySQL)導(dǎo)入到MySQL數(shù)據(jù)庫中 2.利用達夢的數(shù)據(jù)遷移工具把MySQL庫中的數(shù)據(jù)遷移到達夢數(shù)據(jù)庫中 3.利用達夢數(shù)據(jù)庫遷移工具把達夢數(shù)據(jù)庫中的數(shù)據(jù)導(dǎo)出到sql文件,此時sql文件中的sql語句就可在達夢數(shù)據(jù)庫中執(zhí)行 1. 數(shù)據(jù)遷移如果安裝時選擇安裝了客戶端工具,則會生成一些客戶端操作工具,如遷移工具、DM管理工具、SQL交互式查詢工具等。遷移時選擇DM數(shù)據(jù)遷移工具,按照工具內(nèi)的步驟,選擇MySQL服務(wù)和數(shù)據(jù)庫以及要遷移的DM數(shù)據(jù)庫。 1.新建遷移,按需選擇,我這邊是MySQL -> DM。 2.選擇數(shù)據(jù)源遷移時可以指定Mysql數(shù)據(jù)庫的驅(qū)動,配置一下jdbc驅(qū)動和連接參數(shù)即可。達夢的話就是用默認驅(qū)動即可。 3.遷移策略,可選擇保持對象名大小寫,如果MySQL中表字段有用到 4.后面選擇遷移模式的話全選即可,沒什么需要特殊注意的點 2.數(shù)據(jù)導(dǎo)出第三步遷移完成后,此時達夢數(shù)據(jù)庫已經(jīng)有和MySQL同名的庫(dm中是schema概念)和表數(shù)據(jù)了。接下來要把庫中的數(shù)據(jù)導(dǎo)出為 此時仍然需要用達夢的數(shù)據(jù)遷移工具,新建數(shù)據(jù)遷移,選擇數(shù)據(jù)遷移方式為DM -> SQL,然后指定需要遷移的數(shù)據(jù)源(達夢中的scheme),然后導(dǎo)出到目標(biāo)文件即可。 3. 項目適配(重點)1. 庫名問題問題:導(dǎo)出后的達夢sql腳本你會發(fā)現(xiàn),建表語句格式為 -- mysql CREATE TABLE `T_USER_TEST` ( "id" BIGINT NOT NULL AUTO_INCREMENT,//主鍵自增 "name" VARCHAR(255) NULL ); -- 達夢 CREATE TABLE "MY_DB"."T_USER_TEST" ( "id" BIGINT IDENTITY(1,2) NOT NULL,//主鍵自增 "name" VARCHAR(255) NULL ); 解決方案:創(chuàng)建一個用戶,用戶名為庫名,創(chuàng)建用戶后達夢會自動創(chuàng)建一個和用戶名相同的庫,此時用此用戶登錄連接,執(zhí)行sql語句時表名前面就不需要加庫名了,因為他默認查的就是此用戶下的庫。語句如下(包括創(chuàng)建表空間、賦權(quán)等),后續(xù)連接時使用此賬號和密碼以及url連接中的schema(MY_DB) -- 創(chuàng)建表空間MY_DB CREATE tablespace MY_DB DATAFILE 'MY_DB.DBF' SIZE 128; -- 創(chuàng)建用戶MY_DB,密碼為123456,此時會自動創(chuàng)建名為MY_DB的schema CREATE USER "MY_DB" IDENTIFIED BY "123456" DEFAULT tablespace MY_DB; -- 為MY_DB用戶賦權(quán) grant "DBA","RESOURCE","PUBLIC","SOI" to "MY_DB" with admin option; grant EXECUTE on "SYS"."DBMS_XMLGEN" to "MY_DB"; Spring數(shù)據(jù)庫連接配置參考: #dm8連接 spring.datasource.url=jdbc:dm://127.0.0.1:5236/MY_DB spring.datasource.username=MY_DB spring.datasource.password=123456 spring.datasource.driver-class-name=dm.jdbc.driver.DmDriver spring.datasource.type=com.alibaba.druid.pool.DruidDataSource #如果項目中有使用到JPA,參考如下方言配置 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.DmDialect spring.jpa.properties.hibernate.hbn2ddl.auto=none spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false 2. 主鍵自增問題1. 問題剖析首先,達夢數(shù)據(jù)庫是支持主鍵自增的,DDL中自增關(guān)鍵字為 -- 如下建表語句,id為自增id CREATE TABLE "MY_DB"."t_user_test" ( "id" BIGINT IDENTITY(1,2) NOT NULL,//主鍵自增 "name" VARCHAR(255) NULL ); -- 1.insert的正確姿勢,此時會生成則增id insert into "t_user_test"(name) values("tom"); -- 2.錯誤示范,此時會報錯:僅當(dāng)指定列列表,且SET IDENTITY_INSERT為ON時,才能對自增列賦值 insert into "t_user_test"(id,name) values(1,"tom"); -- 3.錯誤示范,此時會報錯: 僅當(dāng)指定列列表,且SET IDENTITY_INSERT為ON時,才能對自增列賦值或者違反列[id]非空約束 insert into "t_user_test"(id,name) values(null,"tom"); 1.第一種插入沒問題,無可厚非 2.第二種插入會報錯,意思就是說,你的id設(shè)置的為自增列,但是你插入時對自增列手動賦值,這是不允許的,設(shè)置了自增就應(yīng)該用數(shù)據(jù)庫的自增生成。但是項目中難免有手動設(shè)置id插入的場景,此時也是有解決方案的,就是在插入之前設(shè)置IDENTITY_INSERT為ON。注意IDENTITY_INSERT關(guān)鍵字是表級別的關(guān)鍵字,語法要指定到表,不能對全庫進行設(shè)置。 -- 設(shè)置t_user_test表 SET IDENTITY_INSERT MY_DB.t_user_test ON insert into("id","name") values(1,"tom"); -- OFF可以不執(zhí)行,不影響 SET IDENTITY_INSERT MY_DB.t_user_test OFF 針對IDENTITY_INSERT問題,本人做了一些測試,得出以下結(jié)論供參考:
開啟語句:SET IDENTITY_INSERT db.table ON 關(guān)閉語句:SET IDENTITY_INSERT db.table OFF 3.第三種插入報錯很明顯,當(dāng)你沒有設(shè)置IDENTITY_INSERT時,他會先報錯讓你對其設(shè)置為ON,如果設(shè)置完后就會報錯違反id非空約束,因為id建表時為主鍵,自帶非空約束。不能顯示插入null值,此種錯誤只能對sql進行處理,后面會講。 2. 問題處理經(jīng)過以上問題分析,insert某張表時,可以先設(shè)置IDENTITY_INSERT為ON,雖然只有第一種insert不需要設(shè)置,可以直接走自增,但是你設(shè)置后也不會影響insert的執(zhí)行,為了偷懶不想整理項目中的sql,索性所有insert都設(shè)置IDENTITY_INSERT為ON。當(dāng)然你可以寫sql,修改項目中的代碼,在所有insert操作之前都執(zhí)行一遍INDENTITY_INSERT ON,但是代碼中持久層框架用了JPA和Mybatis,并且此類sql很多,所以采用AOP的方式解決。
解決思路:在我們項目中使用JPA保存對象實現(xiàn)插入都是間接調(diào)用JpaRepository.save()方法,所以在此方法加一層攔截處理就行了,執(zhí)行save之前先執(zhí)行SET IDENTITY_INSERT ON,參考代碼如下: @Aspect @Component public class JpaSaveAspect { public static final String IDENTITY_INSERT_ON = "SET IDENTITY_INSERT MY_DB.%s ON"; public static final String IDENTITY_INSERT_OFF = "SET IDENTITY_INSERT MY_DB.%s OFF"; @Autowired private JdbcTemplate jdbcTemplate; // 節(jié)點為JpaRepository.save @Pointcut("execution(* org.springframework.data.jpa.repository.JpaRepository.save(..))") public void savePointcut() { } //執(zhí)行切點方法之前要進行的處理 @Before("savePointcut()") public void beforeSave(JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); if (Objects.isNull(args) || args.length != 1) { return; } Object obj = args[0]; Class<?> clazz = obj.getClass(); Annotation[] annotations = clazz.getAnnotations(); Long id = null; try { //通過反射獲取save的實體對象,并通過getId方法獲取里面的id值,也就是主鍵值 Method method = clazz.getMethod("getId"); id = (Long) method.invoke(obj); } catch (Exception e) { } // 當(dāng)id(主鍵)為空時,不需要處理,因為此時走的數(shù)據(jù)庫的自增 if (Objects.isNull(id) || id <= 0){ return; } for (Annotation annotation : annotations) { // 獲取JPA實體的@Tabel注解,解析出表名 if (annotation instanceof Table) { Table tableAnnotation = (Table) annotation; //表名拼接進sql進行執(zhí)行,SET IDENTITY_INSERT MY_DB.t_user ON String identityInsertOn = String.format(IDENTITY_INSERT_ON, tableAnnotation.name()); log.warn("JPA IDENTITY_INSERT_ON:{}", identityInsertOn); jdbcTemplate.execute(identityInsertOn); } } } }
解決思路:Mybatis提供的有自己的攔截器,也叫插件,只需要自定義攔截器即可,使用方式是實現(xiàn)org.apache.ibatis.plugin.Interceptor接口并注冊為Bean,并在Mybatis的SqlSessionFactory設(shè)置此攔截器使其生效。對這塊不熟的可以網(wǎng)上看看相關(guān)資料。接下來攔截器中就可以攔截sql并在sql執(zhí)行之前做處理了。參考代碼如下: 代碼處理的問題: 1.處理非法字符,如刪掉sql中的`字符 2.處理boolean參數(shù),達夢的bit類型對應(yīng)java中的boolean類型,把sql中的true和false關(guān)鍵字替換為1和0 3.處理主鍵自增 在執(zhí)行insert之前執(zhí)行SET IDENTITY_INSERT,由于本人對Mybatis不太熟,沒在攔截器中找到sqlSersion對象,也就沒法通過sqlSersion來執(zhí)行我自定義的sql。而通過調(diào)用jdbcTemplate等三方執(zhí)行,可能導(dǎo)致兩個sql不在一個會話中執(zhí)行,也就導(dǎo)致可能你執(zhí)行的SET IDENTITY_INSERT不在此會話生效(看上面IDENTITY_INSERT的測試結(jié)果),此時你可以通過在insert 語句所在的方法加事務(wù)嘗試解決。目前我是通過拼接sql方式解決,在攔截器中把解析出來的sql前面拼接自定義sql。(會話的問題和事務(wù)我也只是猜測,并沒實際驗證,僅供參考) /** * @description: mybatis sql攔截器,作用有三種:1.處理非法字符 2.處理boolean參數(shù) 3.處理插入主鍵自增問題 */ @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) @Slf4j @Component public class MybatisSqlInterceptor implements Interceptor { /** * 正則不區(qū)分大小寫匹配"=true",包括=中間有空白字符 */ private static final String SQL_TRUE_PARAM_REG = "(?i)=\\s*true"; /** * 正則不區(qū)分大小寫匹配"=false",包括=中間有空白字符 */ private static final String SQL_FALSE_PARAM_REG = "(?i)=\\s*false"; /** * 正則匹配insert into和merge into語句 */ private static final String SQL_INSERT_REG = "(?i)(insert into|merge into)\\s+([^\\s]+)"; /** * 開啟insert開關(guān) */ private static final String IDENTITY_INSERT_ON = "SET IDENTITY_INSERT MY_DB.%s ON;"; /** * 無主鍵的關(guān)聯(lián)表 */ private Set<String> identityInsertExcludeTableSet; //配置,可配置庫中無自增鍵的表,把它過濾掉,因為這些表沒有主鍵自增問題 @Value("${mybatis.insert.exclude.table:t_no_identity_table_test}") private String excludeTable; @PostConstruct public void initExcludeTableSet() { //加載時將excludeTable的表放入HaseSet,提升后續(xù)匹配效率 identityInsertExcludeTableSet = Arrays.stream(excludeTable.split(",")) .collect(Collectors.toSet()); } @Override public Object intercept(Invocation invocation) throws Exception { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); BoundSql boundSql = statementHandler.getBoundSql(); String sql = this.handleIllegalChar(boundSql.getSql()); if (sqlCommandType == SqlCommandType.SELECT) { sql = this.handleBooleanParam(sql); } if (sqlCommandType == SqlCommandType.INSERT) { sql = this.handleIdentityInsertOn(sql); } metaObject.setValue("delegate.boundSql.sql", sql); return invocation.proceed(); } /** * 處理非法字符 * * @param sql * @return: java.lang.String */ private String handleIllegalChar(String sql) { return sql.replace("`", ""); } /** * 處理插入時自增id開關(guān)問題 * * @param sql */ private String handleIdentityInsertOn(String sql) { String tableName = null; Pattern pattern = Pattern.compile(SQL_INSERT_REG, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(sql); if (matcher.find()) { tableName = matcher.group(2); } if (StringUtils.isNotBlank(tableName) && !identityInsertExcludeTableSet.contains(tableName)) { String identityInsertOn = String.format(IDENTITY_INSERT_ON, tableName); log.warn("Mybatis IDENTITY_INSERT_ON:{}", identityInsertOn); sql = identityInsertOn + sql; } return sql; } /** * 處理sql中的布爾值 * * @param sql * @return: java.lang.String */ private String handleBooleanParam(String sql) { return sql.replaceAll(SQL_TRUE_PARAM_REG, "= 1") .replaceAll(SQL_FALSE_PARAM_REG, "= 0"); } } 3. SQL語法相關(guān)問題安裝時能要求大小寫不敏感盡量選擇大小寫不敏感,不然建表時字段都要用大寫,如果用小寫,查詢時字段用小寫查可能會報錯:無效列名等 1. 字段column字段名稱關(guān)鍵字沖突數(shù)據(jù)庫中都有一些自己的關(guān)鍵字,如果建的表中有些關(guān)鍵字和數(shù)據(jù)庫中的沖突,就有可能執(zhí)行某些sql報錯,此類沖突的關(guān)鍵字盡量手動改掉,以下是我遇到的關(guān)鍵字供參考:
字段類型1)mysql中的 2)bit類型Mysql可使用 2. 函數(shù)及語法1. 2.如果用到 可根據(jù)場景看看是否能刪除group by替換為 3. 4.如果使用到mysql的 5.if語句,達夢支持if語句,但只能支持簡單的場景,如下 -- 支持 where if(id>1,2,3) -- 不支持 where if(1 = 1,status = 2 or status =3 ,1=1) 以上不支持的場景可以用邏輯解決: where ((1 = 1 AND (status = 2 OR status = 3)) OR (1 = 1)) 4. 其它問題我們項目中用到了clickhouse數(shù)據(jù)庫,并且使用了clickhouse的字典表連接了外部數(shù)據(jù)庫,也就是Mysql中的某些表,作用是可以吧mysql某些表里的某些數(shù)據(jù)同步到clickhouse映射表中,并且建立好映射表后,后期clickhouse中表的數(shù)據(jù)可自動同步mysql表中的數(shù)據(jù),如不了解的可去ck官網(wǎng)查看 https:///docs/zh/sql-reference/dictionaries/external-dictionaries/external-dicts-dict-sources#dicts-external_dicts_dict_sources-mysql 問題:clickhouse內(nèi)置支持mysql的字典表,但不支持達夢8, 解決:clickhouse提供了bridge方式,如clickhouse-jdbc-bridge、clickhouse-odbc-bridge,大概意思就是提供了個中間件,它是以獨立進程來啟動,他來作為ck和外部數(shù)據(jù)庫的橋接來自動同步數(shù)據(jù)。 1. ODBC環(huán)境依賴:unixODBC + 達夢8的odbc驅(qū)動 其中unixODBC可根據(jù)操作系統(tǒng)下載rpm包或者下載源碼進行編譯(網(wǎng)上有教程) odbc驅(qū)動可以從安裝達夢8所在的服務(wù)器上找,安裝目錄下有個drivers文件,里面有各種驅(qū)動,包括odbc,把驅(qū)動文件(.so)以及相關(guān)依賴拷貝到ck服務(wù)器,然后在unixODBC的配置文件中添加dm的數(shù)據(jù)源和驅(qū)動配置路徑,然后再ck中創(chuàng)建字典表,并且指定達夢數(shù)據(jù)源。 這種方式本人在x86機器驗證過,是可行的,但是unixODBC有版本問題,達夢8odbc驅(qū)動是.so文件鏈接庫,同時有依賴其他鏈接庫,操作不好就會有鏈接缺失的問題。本人就是后面x86驗證后,拿到aarch64架構(gòu)機器去驗證時,依賴的加解密so庫和系統(tǒng)中內(nèi)置的沖突了,但是又沒找到法子對其進行環(huán)境隔離,故后面放棄了。 2.JDBC實際中本人是采用這種方式,開始沒采用是因為當(dāng)時看到了jdbc-bridge,但還是想找一種字典表的方式,想著看看字典表支不支持配置自定義連接數(shù)據(jù)源,就越走越遠,后面又用了ODBC開始踩坑,一直踩到國產(chǎn)環(huán)境編譯動態(tài)庫后沖突問題,作為java程序員已經(jīng)走不動了,就驀然回首從0開始踩坑jdbc,中間jdbc還有一些踩坑歷程就不說了,下面直接說結(jié)論吧。 clickhouse-jdbc-bridge源碼地址:https://github.com/ClickHouse/clickhouse-jdbc-bridge 源碼地址有說明,因為是采用java代碼編寫的,所以沒有跨平臺的問題,直接下載rpm包安裝即可: wget https://github.com/ClickHouse/clickhouse-jdbc-bridge/releases/download/v2.1.0/clickhouse-jdbc-bridge-2.1.0-1.noarch.rpm rpm -ivh clickhouse-jdbc-bridge-2.1.0-1.noarch.rpm 1.安裝后需要把達夢8的jdbc驅(qū)動放入某個文件,后面配置要指定此驅(qū)動 2.在jdbc-bridge的安裝目錄(默認為/etc/clickhouse-jdbc-bridge/config/datasources),新建.json文件,里面配置你的數(shù)據(jù)庫相關(guān)連接(驅(qū)動、url、賬號、密碼等信息) 3.運行啟動clickhouse-jdbc-bridge(默認端口9019) 3.配置clickhouse的config.xml文件,配置jdbc-bridge連接,重啟clickhouse服務(wù) <jdbc_bridge> <host>127.0.0.1</host> <port>9019</port> </jdbc_bridge> 4.在clickhouse客戶端執(zhí)行建表語句,示例如下,其中dm8參數(shù)是clickhouse-jdbc-bridge數(shù)據(jù)源配置的名稱,DM_DB是達夢數(shù)據(jù)庫的schema名,后面是將查詢的結(jié)果放入ck表,這個位置也可以直接寫表名。 CREATE TABLE ck_user_test ( id UInt64, name String ) ENGINE = JDBC('dm8', 'DM_DB', 'select id,name from t_dm_user_test WHERE xxx=0') 建表成功后再ck中就可以查詢ck_user_test這張表了,數(shù)據(jù)同步周期可配置,具體其他配置可參考官網(wǎng)或自行百度 |
|