寫一個工具生成資料庫實體類
寫一個java工具從資料庫生成實體類
開始寫之前的說明
這只是一個簡單的解析資料庫建表語句之後,根據解析結果生成java檔案的一個簡單工具。寫的原因有兩個。
1:專案中沒有人寫實體類的註釋,欄位的註釋,現有的工具也沒法根據資料庫的註釋自動新增到class檔案上。
2:自己寫一個也似乎不是很難。
所以就自己寫了一個。
這裡在生成java檔案的時候用的是freemarker 。用了jdbc 作為執行sql的工具。
這個專案已經放在github上了,地址:https://github.com/hjx601496320/entityMaker 。
用到的依賴:
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.28</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.45</version> </dependency> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency>
獲取資料庫中的所有的表名稱
想要根據資料庫中的建表語句來建立java檔案首先要先知道資料庫中都有那些表。so,開始。
配置資料庫的相關資訊
在獲取資料庫連結前,我們先寫一個檔案用來儲存資料庫連結的各種資訊。
新建檔案config.xml
<xml> <jdbc.url></jdbc.url> <jdbc.username></jdbc.username> <jdbc.password></jdbc.password> </xml>
jdbc.url:連結資料庫的url。例如:jdbc:mysql://127.0.0.1:3306/demo?useSSL=true
jdbc.username:資料庫的使用者名稱。
jdbc.password:資料庫的密碼。
這樣就配置好了。
寫一個工具類讀取xml:XmlUtils.java
import org.w3c.dom.Document; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Scanner; /** * 讀取xml */ public class XmlUtils { /** * 讀取 Document * * @param xmlPath * @return */ public static Document getConfigDocument(String xmlPath) { try { InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(xmlPath); Scanner scanner = new Scanner(resourceAsStream); StringBuilder stringBuilder = new StringBuilder(); while (scanner.hasNextLine()) { stringBuilder.append(scanner.nextLine()).append("\n"); } DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder documentBuilde = documentBuilderFactory.newDocumentBuilder(); Document document = documentBuilde.parse(new ByteArrayInputStream(stringBuilder.toString().getBytes())); return document; } catch (Exception e) { e.printStackTrace(); return null; } } }
開啟資料庫連結
//獲取資料庫配置資訊 Document configXml = XmlUtils.getConfigDocument(CONFIG_PATH); Element element = configXml.getDocumentElement(); String jdbcUrl = element.getElementsByTagName("jdbc.url").item(0).getTextContent(); String username = element.getElementsByTagName("jdbc.username").item(0).getTextContent(); String password = element.getElementsByTagName("jdbc.password").item(0).getTextContent(); //開啟資料庫連結 Connection conn = (Connection) DriverManager.getConnection(jdbcUrl, username, password);
獲取到連結之後,下一步就是要讀取資料庫中的表資料了。
獲取資料庫中的表
Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SHOW TABLES;"); while (resultSet.next()) { //這裡就獲取到了資料庫中的所有的表的名稱了。 String tableName = resultSet.getString(1); }
這裡拿到表名稱後就可以依次得到建表語句,並解析建表語句了。
這裡之所以使用解析建表語句的方法是因為這樣可以比較完整的得到註釋資訊。用另一種方法的時候表的註釋一直獲取不到(另一種方法我忘記怎麼說了~)。
得到建表語句
這裡就是拼接sql,然後執行就好了。下面是程式碼:
Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("show CREATE TABLE 表名稱"); while (resultSet.next()) { //這裡就得到了表的建表語句 String createTableSql = resultSet.getString(2); }
做到這一步,我們就完整的得到了資料庫中的所有的建表語句了。接下來就是要分析建表語句並且用來生成實體類了。
建表語句分析
建表sql
下面是執行 sqlshow create table user 的結果:
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '使用者id', `name` varchar(225) DEFAULT NULL COMMENT '使用者名稱', `create_date` datetime DEFAULT NULL, `status` int(11) DEFAULT NULL, `age` int(11) DEFAULT NULL COMMENT '年齡', `mark` varchar(225) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2104778081 DEFAULT CHARSET=latin1 COMMENT='使用者表'
這裡可以看出,要建立的實體類的名稱就在第一行的CREATE TABLE 後的兩個` 符號中間,這樣我們就可以通過正則將表的名稱取出來,然後轉換成為我們需要的class名稱。首先我們先寫一個通過正則提取資料的方法,下面是程式碼:
正則程式碼
/** * 根據正則查詢 * * @param sql * @param pattern * @param group * @return */ static String getByPattern(String sql, String pattern, int group) { Pattern compile = Pattern.compile(pattern); Matcher matcher = compile.matcher(sql); while (matcher.find()) { return matcher.group(group); } return null; }
現在開始從建表語句中提取table的名稱(雖然在獲得資料庫所有表的時候就已經知道了,但是在寫一次也沒有什麼問題不是嗎 ~~~),下面是程式碼:
獲取表的名稱
/** * 獲得表的名稱 * * @param sql * @return */ public static String getTableName(String sql) { return getByPattern(sql, "CREATE TABLE `(.*)`", 1); }
這裡就已經將表的名稱取出來了。
接下來是獲取表上的註釋,這裡我們取表的註釋。下面是程式碼:
提取表註釋
public static String getTableComment(String sql) { return getByPattern(sql, "\\) .* COMMENT='(.*)'", 1); }
現在開始獲取id資訊。
獲取id
獲取id依然是使用正則就好了,程式碼如下:
public static String getId(String sql) { return getByPattern(sql, "PRIMARY KEY \\(`(.*)`\\)", 1); }
因為原本語句中有一對括號,所以在這裡對外面的括號做了轉義處理。接下來開始提取資料庫中的欄位,欄位型別,欄位註釋。
欄位,欄位型別,欄位註釋
先取出來建表語句中的和欄位相關的sql
2019年01月21日,修改:
沒想到有的表裡沒有id,就導致下面的程式碼執行後出錯了,修改一下結束的判斷。
/** * 獲取建表語句中和欄位相關的sql * * @param sql * @return */ public static List<String> getColumnSqls(String sql) { List<String> lines = new ArrayList<>(); Scanner scanner = new Scanner(sql); boolean start = false; while (scanner.hasNextLine()) { String nextLine = scanner.nextLine(); if (nextLine.indexOf("CREATE TABLE") != -1) { start = true; continue; } //沒想到有的表沒有id /(ㄒoㄒ)/~~ if (nextLine.indexOf("PRIMARY KEY") != -1 || nextLine.indexOf("ENGINE=") != -1) { start = false; continue; } if (start) { lines.add(nextLine); } } return lines; }
這裡的執行結果是:
id` int(11) NOT NULL AUTO_INCREMENT COMMENT '使用者id', `name` varchar(225) DEFAULT NULL COMMENT '使用者名稱', `create_date` datetime DEFAULT NULL, `status` int(11) DEFAULT NULL, `age` int(11) DEFAULT NULL COMMENT '年齡', `mark` varchar(225) DEFAULT NULL,
這也就取到了table中所有的欄位相關資訊了,接下來我們來獲取欄位名稱:
獲取列名,註釋,資料型別
List<String> columns = SqlUtils.getColumnSqls(sql); for (String oneLine : columns) { System.out.println(oneLine); String columnName = SqlUtils.getByPattern(oneLine, "`(.*)`", 1); String comment = SqlUtils.getByPattern(oneLine, "COMMENT '(.*)'", 1); String columnType = SqlUtils.getByPattern(oneLine, "`" + columnName + "` ([A-Za-z]*)", 1); System.out.printf("名稱:%-20s 型別:%-20s 註釋:%-20s \n", columnName, columnType, comment); }
輸出結果:
id` int(11) NOT NULL AUTO_INCREMENT COMMENT '使用者id', 名稱:id型別:int註釋:使用者id `name` varchar(225) DEFAULT NULL COMMENT '使用者名稱', 名稱:name型別:varchar註釋:使用者名稱 `create_date` datetime DEFAULT NULL, 名稱:create_date型別:datetime註釋:null `status` int(11) DEFAULT NULL, 名稱:status型別:int註釋:null `age` int(11) DEFAULT NULL COMMENT '年齡', 名稱:age型別:int註釋:年齡 `mark` varchar(225) DEFAULT NULL, 名稱:mark型別:varchar註釋:null
到了這,我就就已經從建表語句裡拿到了所有需要的資料了,下面就開始使用這些資料來生成java檔案了。
根據上面獲取的資料開始建立java檔案
終於開始要建立java檔案了。
但是~在建立java檔案的時候要先吧之前獲取的數稍微處理一下,將sql中的格式轉換為java中的格式。比如屬性名稱,資料型別,class名稱之類的,現在開始~
將表名稱轉換為合適的class名稱
就是首字母大寫,駝峰式的命名規範。例如將user_log 或者USER_LOG 轉換為UserLog。
我們可以這麼寫:
/** * 類名稱轉換 * * @param tableName * @return */ public static String entityName(String tableName) { String lowerCaseName = tableName.toLowerCase(); StringBuilder newName = new StringBuilder(); char[] chars = lowerCaseName.toCharArray(); boolean change = false; for (int i = 0; i < chars.length; i++) { char aChar = chars[i]; if (aChar == '_' && !change) { change = true; continue; } //首字母大寫 if (i == 0) { aChar = Character.toUpperCase(aChar); } if (change) { aChar = Character.toUpperCase(aChar); change = false; } newName.append(aChar); } return newName.toString(); }
這樣就得到了我們需要的class的名稱了。
將欄位名稱轉換為java中的屬性名稱
這裡就是將上一步操作的首字母大寫去掉就好了,下面是程式碼:
/** * 屬性名稱轉換 * * @param name * @return */ public static String fieldName(String name) { name = name.toLowerCase(); StringBuilder newName = new StringBuilder(); char[] chars = name.toCharArray(); boolean change = false; for (int i = 0; i < chars.length; i++) { char aChar = chars[i]; if (aChar == '_' && !change) { change = true; continue; } if (change) { aChar = Character.toUpperCase(aChar); change = false; } newName.append(aChar); } return newName.toString(); }
接下來是將sql中的資料型別轉換為java中的資料型別。
sql資料型別轉換
這裡用map做了一個對映,有自己特定要求的可以自己修改。
public class ColumnFieldTypeMapping { private Map<String, Class> sqlFieldTypeMapping = new HashMap<>(); { sqlFieldTypeMapping.put("VARCHAR", String.class); sqlFieldTypeMapping.put("CHAR", String.class); sqlFieldTypeMapping.put("TEXT", String.class); sqlFieldTypeMapping.put("MEDIUMTEXT", String.class); sqlFieldTypeMapping.put("LONGTEXT", String.class); sqlFieldTypeMapping.put("TINYTEXT", String.class); sqlFieldTypeMapping.put("BIT", Boolean.class); sqlFieldTypeMapping.put("INT", int.class); sqlFieldTypeMapping.put("BIGINT", long.class); sqlFieldTypeMapping.put("DOUBLE", double.class); sqlFieldTypeMapping.put("TINYINT", int.class); sqlFieldTypeMapping.put("FLOAT", float.class); sqlFieldTypeMapping.put("DECIMAL", BigDecimal.class); sqlFieldTypeMapping.put("INT UNSIGNED", int.class); sqlFieldTypeMapping.put("BIGINT UNSIGNED", int.class); sqlFieldTypeMapping.put("DECIMAL UNSIGNED", BigDecimal.class); sqlFieldTypeMapping.put("DATETIME", Date.class); sqlFieldTypeMapping.put("TIME", Date.class); sqlFieldTypeMapping.put("DATE", Date.class); sqlFieldTypeMapping.put("TIMESTAMP", Date.class); } /** * 根據sql資料型別獲取Java資料型別 * * @param columnType * @return */ public Class getFieldType(String columnType) { Class aClass = sqlFieldTypeMapping.get(columnType); if (aClass == null) { return sqlFieldTypeMapping.get(columnType.toUpperCase()); } return null; } }
寫到這裡,所有參與生成java檔案的資訊就已經獲取完成了。
這時候我們需要把他們組裝起來,用來放進freemarker中來解析並生成java檔案中的內容。
組裝引數
這裡可能我以後用這個程式碼幹別的事情所以我建了兩個類,一個是ClassModel.java ,一個是EntityModel.java 。
EntityModel繼承了ClassModel。我們主要用的是EntityModel.java 。下面是程式碼:
import java.util.*; /** * 用於生成java Entity檔案的類 */ public class ClassModel { /** * java 中不需要引包的型別 */ private static List<Class> baseClass = Arrays.asList( int.class, double.class, float.class, long.class, short.class, byte.class, char.class, boolean.class, String.class ); /** * 類註釋 */ private String classDoc; /** * 類名 */ private String className; /** * 類 包名 */ private String packageName; /** * K:屬性名稱 * V:屬性型別 */ private Map<String, Class> fields = new HashMap<>(); /** * 屬性的註釋 */ private Map<String, String> fieldDoc = new HashMap<>(); ; private List<Class> imports = new ArrayList<>(); /** * 新增需要匯入的包 * * @param importClass */ public void addImport(Class importClass) { if (baseClass.indexOf(importClass) != -1) { return; } if (imports.indexOf(importClass) == -1) { imports.add(importClass); } } /** * 新增屬性 * * @param fieldName屬性名稱 * @param fieldClass 屬性型別 */ public void addfield(String fieldName, Class fieldClass) { if (!fields.containsKey(fieldName)) { fields.put(fieldName, fieldClass); } } /** * 新增屬性註釋 * * @param fieldName 屬性名稱 * @param fieldDoc屬性註釋 */ public void addfieldDoc(String fieldName, String fieldDoc) { if (!this.fieldDoc.containsKey(fieldName)) { this.fieldDoc.put(fieldName, fieldDoc); } } public List<Class> getImports() { return imports; } public void setImports(List<Class> imports) { this.imports = imports; } public String getClassDoc() { return classDoc; } public void setClassDoc(String classDoc) { this.classDoc = classDoc; } public String getClassName() { return className; } public void setClassName(String className) { this.className = className; } public String getPackageName() { return packageName; } public void setPackageName(String packageName) { this.packageName = packageName; } public Map<String, Class> getFields() { return fields; } public void setFields(Map<String, Class> fields) { this.fields = fields; } public Map<String, String> getFieldDoc() { return fieldDoc; } public void setFieldDoc(Map<String, String> fieldDoc) { this.fieldDoc = fieldDoc; } @Override public String toString() { final StringBuilder sb = new StringBuilder("{"); sb.append("\"classDoc\"=\"").append(classDoc).append('\"'); sb.append(",\"className\"=\"").append(className).append('\"'); sb.append(",\"packageName\"=\"").append(packageName).append('\"'); sb.append(",\"fields\"=").append(fields); sb.append(",\"fieldDoc\"=").append(fieldDoc); sb.append(",\"imports\"=").append(imports); sb.append('}'); return sb.toString(); } }
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 資料庫對映 */ public class EntityModel extends ClassModel { /** * 資料庫名稱 */ private String tableName; /** * 資料庫中的Id欄位名稱 */ private List<String> idColumnNames = new ArrayList<>(); /** * 類屬性名對應資料庫欄位對映 * key: class 屬性名稱 * value:資料庫欄位名 */ private Map<String, String> fieldSqlName = new HashMap<>(); /** * 新增class 屬性對映和 資料庫 欄位對映 * * @param fieldName * @param sqlName */ public void addfieldSqlName(String fieldName, String sqlName) { if (!fieldSqlName.containsKey(fieldName)) { fieldSqlName.put(fieldName, sqlName); } } /** * 新增id欄位名 * * @param idColumnName */ public void addIdColumnName(String idColumnName) { idColumnNames.add(idColumnName); } public String getTableName() { return tableName; } public void setTableName(String tableName) { this.tableName = tableName; } public Map<String, String> getFieldSqlName() { return fieldSqlName; } public void setFieldSqlName(Map<String, String> fieldSqlName) { this.fieldSqlName = fieldSqlName; } public List<String> getIdColumnNames() { return idColumnNames; } public void setIdColumnNames(List<String> idColumnNames) { this.idColumnNames = idColumnNames; } }
在這裡將從資料庫中得到的資料都組裝好,就可以使用freemarker來生成Java檔案的內容了。下面是程式碼:
/** * 根據建表語句組裝EntityModel * * @param createTableSql * @return */ EntityModel makeModelBySql(String createTableSql) { Formatter formatter = new Formatter(); EntityModel model = new EntityModel(); String tableComment = SqlUtils.getTableComment(createTableSql); String tableName = SqlUtils.getTableName(createTableSql); String id = SqlUtils.getId(createTableSql); model.addIdColumnName(id); model.setClassName(NameConvert.entityName(tableName)); model.setTableName(tableName); //註釋是null的時候用資料庫表名作為註釋 model.setClassDoc(tableComment == null ? tableName : tableComment); List<String> line = SqlUtils.getColumnSqls(createTableSql); for (String oneLine : line) { String columnName = SqlUtils.getByPattern(oneLine, "`(.*)`", 1); String comment = SqlUtils.getByPattern(oneLine, "COMMENT '(.*)'", 1); String columnType = SqlUtils.getByPattern(oneLine, "`" + columnName + "` ([A-Za-z]*)", 1); String fieldName = NameConvert.fieldName(columnName); Class fieldClass = columnFieldTypeMapping.getFieldType(columnType); if (fieldClass == null) { formatter.format("table:%s columnName:%s sql型別:%s 沒有對映型別", tableName, columnName, columnType); throw new UnsupportedOperationException(formatter.toString()); } model.addfield(fieldName, fieldClass); //欄位註釋是null的時候用資料庫欄位名作為註釋 model.addfieldDoc(fieldName, comment == null ? columnName : comment); model.addfieldSqlName(fieldName, columnName); model.addImport(fieldClass); } return model; }
這樣一個我們需要的引數就組裝好了。現在開始編寫freemarker用的程式碼。
freemarker工具類
用來載入freemarker模板和處理模板中的引數。FreeMarkerUtils.java ,程式碼如下:
import freemarker.cache.StringTemplateLoader; import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapper; import freemarker.template.Template; import java.io.StringWriter; import java.io.Writer; import java.util.Locale; import java.util.Scanner; public class FreeMarkerUtils { /** * freemarker工具, * * @param subjectParams * @param templetPath * @return * @throws Exception */ public static String getJavaClass(Object subjectParams, String templetPath) throws Exception { StringTemplateLoader loader = new StringTemplateLoader(); Scanner scanner = new Scanner(Thread.currentThread().getContextClassLoader().getResourceAsStream(templetPath)); StringBuilder builder = new StringBuilder(); while (scanner.hasNext()) { builder.append(scanner.nextLine()).append("\n"); } String name = System.currentTimeMillis() + ""; loader.putTemplate(name, builder.toString()); //第一步:例項化Freemarker的配置類 Configuration conf = new Configuration(); conf.setObjectWrapper(new DefaultObjectWrapper()); conf.setLocale(Locale.CHINA); conf.setDefaultEncoding("utf-8"); conf.setTemplateLoader(loader); //處理空值為空字串 conf.setClassicCompatible(true); Template template = conf.getTemplate(name); Writer out = new StringWriter(2048); template.process(subjectParams, out); String javaClass = out.toString(); return javaClass; } }
現在有了工具類之後,還不能立即開始生成java檔案,因為還要繼續設定java的package和生成檔案的路徑,這時候我們可以修改之前寫的config.xml
修改config.xml
<xml> <jdbc.url></jdbc.url> <jdbc.username></jdbc.username> <jdbc.password></jdbc.password> <basePath>/home/hjx/work/demo/src/main/java</basePath> <entityPackage>top.hejiaxuan.demo.entity</entityPackage> </xml>
這裡添加了兩個引數:basePath 和entityPackage 。一個是要生成java的檔案的路徑,一個是java檔案的包名。
然後我們再寫一個寫出檔案的工具類FileUtils.java
編寫FileUtils.java
import java.io.*; public class FileUtils { /** * 寫入檔案 * * @param path檔案路徑 * @param content 檔案內容 */ public static void write(String path, String content) { File file = new File(path); File parentFile = file.getParentFile(); try { if (!parentFile.exists()) { parentFile.mkdirs(); } if (!file.exists()) { file.createNewFile(); } FileWriter fileWriter = new FileWriter(file); fileWriter.write(content); fileWriter.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
這樣就萬事具備,就差生成檔案啦。下面就開始啦~~~
開始生成java檔案
在生成檔案前,我們還需要把basePath ,entityPackage 從配置檔案裡取出來,這一步我就不寫了~~
static final String DOT = "."; static final String FILE_TYPE = ".java"; static final String ENTITY_TEMPLET_PATH = "EntityTemp.ftl"; /** * 用於生成一個類檔案 * * @param entityModel * @return */ boolean makeOneClass(EntityModel entityModel) { entityModel.setPackageName(entityPackage); String filePath = basePath + "/" + entityPackage.replace(DOT, "/") + "/" + entityModel.getClassName() + FILE_TYPE; try { String javaClassString = FreeMarkerUtils.getJavaClass(entityModel, ENTITY_TEMPLET_PATH); FileUtils.write(filePath, javaClassString); return true; } catch (Exception e) { e.printStackTrace(); } return false; }
好啦~~~大功告成。
額~~~
好像少點啥~~~
模板檔案沒有放出來~~~
編寫EntityTemp.ftl
package ${packageName}; <#--匯入的包--> <#list imports as import> import ${import.name}; </#list> <#--類名--> <#if classDoc?length gt 0> /** * ${classDoc} * @author hejiaxuan */ </#if> public class ${className} { <#--屬性名稱--> <#list fields?keys as key> <#assignfieldDocStr = fieldDoc[key]> <#if fieldDocStr?length gt 0> /**${fieldDocStr}*/ </#if> <#if idColumnNames?seq_contains(fieldSqlName[key])> </#if> private ${fields[key].simpleName} ${key}; </#list> <#list fields?keys as key> <#assignfieldClass = fields[key].simpleName> <#--setter--> public void set${key?cap_first}(${fieldClass} ${key}) { this.${key} = ${key}; } <#--getter--> public ${fieldClass} <#if fieldClass="boolean">is<#else>get</#if>${key?cap_first}() { return this.${key}; } </#list> @Override public String toString() { final StringBuilder sb = new StringBuilder("["); <#list fields?keys as key> sb.append("${key}:").append(${key}).append(";"); </#list> sb.append("]"); return sb.toString(); } }
最後
這裡面我只是貼出來了一些要用到的程式碼片段,沒有將所有的程式碼全部寫出來。其實寫工具就是一個慢慢實現自己思路的過程,有思路的話一切都很簡單。
如果有人需要專案全部程式碼的話請到 https://github.com/hjx601496320/entityMaker 自行檢視。