1. 程式人生 > >懶要懶到底,能自動的就不要手動,Hibernate正向工程完成Oracle資料庫到MySql資料庫轉換(含欄位轉換、註釋)

懶要懶到底,能自動的就不要手動,Hibernate正向工程完成Oracle資料庫到MySql資料庫轉換(含欄位轉換、註釋)

需求描述

需求是這樣的:因為我們目前的一個老專案是Oracle資料庫的,這個庫呢,資料庫是沒有註釋的,而且欄位名和表名都是大寫風格,比如

在程式碼層面的po呢,以前也是沒有任何註釋的,但是經過這些年,大家慢慢踩坑多了,也給po加上了一些註釋了,比如:

現狀就是這樣,再說說目標是:希望把這個庫能轉成mysql,表名和欄位名最好都用下劃線分隔每個單詞,欄位呢,最好能有註釋。也就是差不多下面這樣:

方案分析

最早我嘗試的就是hibernate正向工程,建一個空的mysql庫,然後配置hibernate的選項為:

這樣的話呢,就會自動在我們指定的mysql資料庫生成表了,不過,有兩個瑕疵是:

  1. 生成的表,欄位和表名都是和PO裡一樣的駝峰格式;
  2. 沒有註釋。

第一個問題,我這邊是通過覆蓋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_PositiveEngin