懶要懶到底,能自動的就不要手動,Hibernate正向工程完成Oracle資料庫到MySql資料庫轉換(含欄位轉換、注釋)
- 2019 年 10 月 23 日
- 筆記
需求描述
需求是這樣的:因為我們目前的一個老項目是Oracle資料庫的,這個庫呢,資料庫是沒有注釋的,而且欄位名和表名都是大寫風格,比如
在程式碼層面的po呢,以前也是沒有任何注釋的,但是經過這些年,大家慢慢踩坑多了,也給po加上了一些注釋了,比如:
現狀就是這樣,再說說目標是:希望把這個庫能轉成mysql,表名和欄位名最好都用下劃線分隔每個單詞,欄位呢,最好能有注釋。也就是差不多下面這樣:
方案分析
最早我嘗試的就是hibernate正向工程,建一個空的mysql庫,然後配置hibernate的選項為:
這樣的話呢,就會自動在我們指定的mysql資料庫生成表了,不過,有兩個瑕疵是:
- 生成的表,欄位和表名都是和PO里一樣的駝峰格式;
- 沒有注釋。
第一個問題,我這邊是通過覆蓋hibernate源碼的方式解決,將駝峰轉換為了下劃線;
第二個問題,麻煩一些,因為要做到欄位帶注釋的話,那就得看看哪裡能拿到注釋。hibernate執行過程中,從PO類里?不可能,編譯好的class里,怎麼會有注釋呢?那就只能從源文件著手了,PO類的源碼里,field上是有注釋的,那就必須要去解析PO類的java文件,從裡面提取出每個PO類中:欄位–》注釋的對應關係來。
大方向已定,我們開搞!
最後我這裡解決這兩個問題,是覆蓋了三個hibernate的類的源碼,大概如下:
在繼續之前,先說明一下,這個肯定是要修改hibernate源碼的,這裡只講講怎麼覆蓋某個jar包里的類:
我這裡是spring mvc的老項目,最後是部署在tomcat運行,tomcat的WebAppClassloader,負責載入以下兩個路徑的class:
覆蓋的原理,就是依賴其查找class的先後順序來做,比如lib下的某個jar包有:org.hibernate.mapping.Table這個類,正常情況下,都會載入到這個類;但如果我們在classes下放一個同包名同類名的類,那麼就會優先載入我們的這個class了。但是假設這個類引用了hibernate的其他類B,不影響,畢竟我們沒覆蓋類B,所以還是會到lib下查找,最後還是會使用hibernate jar包中的B。
最終源碼已經放在了:https://github.com/cctvckl/work_util/tree/master/Hibernate_PositiveEngineer
問題1解決步驟:駝峰格式的建表語句轉下劃線
知道怎麼覆蓋了,再說說怎麼去找要覆蓋哪兒,這個需要一點經驗。我這裡先還原成沒修改時的樣子,跑一下項目,發現日誌有以下輸出:
2019-10-23 13:47:11.819 [main] DEBUG [] org.hibernate.SQL - drop table if exists KPIRECORD 2019-10-23 13:47:11.823 [main] DEBUG [] org.hibernate.SQL - create table KPIRECORD ( kpiRecordId varchar(255) not null, endTime varchar(255), evaluatorId varchar(255), kpiComment varchar(255), kpiDate datetime, kpiValue double precision, roleCode integer, startTime varchar(255), superiors varchar(255), userId varchar(255), primary key (kpiRecordId) ) 2019-10-23 13:47:11.988 [main] INFO [] org.hibernate.tool.hbm2ddl.SchemaExport - HHH000230: Schema export complete
其他不重要,我們看最後一行,裡面包含了Schema export complete
,這個肯定是程式碼里的日誌,我們拿這個東西,在程式碼里搜一波(這一步,要求maven是下載了jar包的源碼):
接下來,我們點進去,因為maven下載了源碼的關係,所以再利用idea的findUsage功能,剩下的,就是在覺得比較靠譜的地方打上斷點,運行一下,debug一下,大概就知道流程了。
找啊找,找到了下面的地方,(org.hibernate.mapping.Table#sqlCreateString)
怎麼覆蓋,不用多說了吧,如果是spring mvc(或者spring boot)架構,都要在最上層的module里的src下操作,加上這麼一個全路徑一致的類,然後將裡面的sqlCreateString改寫。
我這裡附上改寫後的:
到這裡,基本搞定了第一個問題。
問題2解決步驟:給建表語句增加註釋
其實這個步驟分成了2個小步驟,第一步是拿到下面這樣的數據:
第二步,就是像上面第一步那樣,在生成create table語句時,根據table名稱,取到上面這樣的數據,然後再根據列名,取到注釋,拼成一條下面這樣的(重點是下面加粗部分):
start_time
varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT ‘考評開始時間’,
問題2解決步驟之第一步:獲取表欄位注釋
這部分純粹考驗字元串解析功力了,我說下思路,也可以直接看源碼。主要是逐行讀取java文件,然後看該行是否為注釋(區分單行注釋和多行注釋):
單行:
/** 被考評人*/ private String userId;
多行:
/** * 主鍵,考評記錄ID */ private String kpiRecordId;
單行注釋的話,直接用正則匹配;多行的話,會引入一個狀態變數,最後還是會轉換為一個單行注釋。
匹配上後,提取出注釋,存到一個全局變數;如果下一行正則匹配了一個field,則將之前的注釋和這個field湊一對,存到map里。
大致流程就是這樣的,程式碼如下:
展開查看
```java package com.ceiec.util; import com.alibaba.fastjson.JSON; import java.io.*; import java.util.ArrayList; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @program: Product * @author: Mr.Fyf * @create: 2018-05-30 16:30 **/ public class CommentUtil { /** * 類名正則 */ static Pattern classNamePattern = Pattern.compile(".*\s+class\s+(\w+)\s+.*\{"); /** * 單行注釋 */ static Pattern singleLineCommentPattern = Pattern.compile("/\*\*\s+(.*)\*/"); /** * field */ static Pattern fieldPattern = Pattern.compile("private\s+(\w+)\s+(.*);"); private static final int MULTI_COMMENT_NOT_START = 0; private static final int MULTI_COMMENT_START = 1; private static final int MULTI_COMMENT_END = 2; public static void main(String[] args) throws IOException { HashMap> commentMap = constructTableCommentMap(); System.out.println(JSON.toJSONString(commentMap)); } public static HashMap > constructTableCommentMap() { HashMap > tableFieldCommentsMap = new HashMap<>(); File dir = new File("F:\workproject_codes\bol_2.0_from_product_version\CAD_Model\src\main\java\com\ceiec\model"); File[] files = dir.listFiles(); try { // for (File fileItem : files) { processSingleFile(fileItem,tableFieldCommentsMap); } // File fileItem = new File("F:\workproject_codes\bol_2.0_from_product_version\SYS_Model\src\main\java\com\ceiec\scm\model\ConsultingParentType.java"); // processSingleFile(fileItem, tableFieldCommentsMap); } catch (Exception e) { } return tableFieldCommentsMap; } public static void processSingleFile(File fileItem, HashMap > tableFieldCommentsMap) throws IOException { FileReader reader = null; try { reader = new FileReader(fileItem); } catch (FileNotFoundException e) { e.printStackTrace(); } BufferedReader bufferedReader = new BufferedReader(reader); String line = null; ArrayList multiLineComments = new ArrayList<>(); int multiLineCommentsState = MULTI_COMMENT_NOT_START; boolean classStarted = false; ArrayList list = new ArrayList<>(); String className = null; String lastSingleLineComment = null; while ((line = bufferedReader.readLine()) != null) { Matcher matcher = classNamePattern.matcher(line); boolean b = matcher.find(); if (b) { className = matcher.group(1); classStarted = true; continue; } if (!classStarted) { continue; } if (line.contains("serialVersionUID")) { continue; } if (multiLineCommentsState == MULTI_COMMENT_NOT_START) { if (line.trim().equals("/**")) { multiLineCommentsState = MULTI_COMMENT_START; continue; } } if (multiLineCommentsState == MULTI_COMMENT_START) { multiLineComments.add(line); if (line.trim().equals("*/") || line.trim().contains("*/")) { for (String multiLineComment : multiLineComments) { if (multiLineComment.trim().equals("/**") || multiLineComment.trim().equals("*/")) { continue; } if (lastSingleLineComment == null) { lastSingleLineComment = multiLineComment; } else { lastSingleLineComment = multiLineComment + lastSingleLineComment; } } lastSingleLineComment = lastSingleLineComment.replaceAll("/", "").replaceAll("\*", "").replaceAll("\t", ""); multiLineComments.clear(); multiLineCommentsState = MULTI_COMMENT_NOT_START; continue; } continue; } Matcher singleLineMathcer = singleLineCommentPattern.matcher(line); boolean b1 = singleLineMathcer.find(); if (b1) { lastSingleLineComment = singleLineMathcer.group(1); continue; } Matcher filedMatcher = fieldPattern.matcher(line); boolean b2 = filedMatcher.find(); if (b2) { String fieldName = filedMatcher.group(2); if (lastSingleLineComment != null) { FieldCommentVO vo = new FieldCommentVO(fieldName, lastSingleLineComment); list.add(vo); lastSingleLineComment = null; } } } if (list.size() == 0) { return; } HashMap fieldCommentMap = new HashMap<>(); for (FieldCommentVO fieldCommentVO : list) { fieldCommentMap.put(fieldCommentVO.getFieldName().toLowerCase(), fieldCommentVO.getComment().trim()); } tableFieldCommentsMap.put(className.toUpperCase(), fieldCommentMap); } } ```
問題2解決步驟之第二步:覆蓋hibernate源碼,建表過程中構造注釋
這次覆蓋了org.hibernate.cfg.Configuration#generateSchemaCreationScript方法:
然后里面的內容也不用我細說了,再次根據列名查找注釋,構造建表sql就行了。
這裡加個成果展示:
總結
希望對大家有所幫助,有疑問可以直接加我。
源碼在:https://github.com/cctvckl/work_util/tree/master/Hibernate_PositiveEngineer