|
|
@@ -39,47 +39,55 @@ import com.jfinal.plugin.activerecord.dialect.MysqlDialect;
|
|
|
* MetaBuilder
|
|
|
*/
|
|
|
public class MetaBuilder {
|
|
|
-
|
|
|
+
|
|
|
protected DataSource dataSource;
|
|
|
protected Dialect dialect = new MysqlDialect();
|
|
|
-
|
|
|
+
|
|
|
// 白名单 + 黑名单选择过滤的 tableName 集合,白名单优先于黑名单
|
|
|
protected Set<String> whitelist = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
|
|
|
protected Set<String> blacklist = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
|
|
|
-
|
|
|
+
|
|
|
protected Predicate<String> tableSkip = null;
|
|
|
-
|
|
|
+
|
|
|
protected Connection conn = null;
|
|
|
protected DatabaseMetaData dbMeta = null;
|
|
|
-
|
|
|
+
|
|
|
protected String[] removedTableNamePrefixes = null;
|
|
|
-
|
|
|
+
|
|
|
protected TypeMapping typeMapping = new TypeMapping();
|
|
|
-
|
|
|
+
|
|
|
protected boolean generateRemarks = false; // 是否生成备注
|
|
|
protected boolean generateView = false; // 是否生成 view
|
|
|
-
|
|
|
+ protected boolean fetchFieldAutoIncrement = false; // 是否取出字段的自增属性
|
|
|
+
|
|
|
public MetaBuilder(DataSource dataSource) {
|
|
|
if (dataSource == null) {
|
|
|
throw new IllegalArgumentException("dataSource can not be null.");
|
|
|
}
|
|
|
this.dataSource = dataSource;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public void setGenerateRemarks(boolean generateRemarks) {
|
|
|
this.generateRemarks = generateRemarks;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public void setGenerateView(boolean generateView) {
|
|
|
this.generateView = generateView;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 配置是否取出字段的自增属性
|
|
|
+ */
|
|
|
+ public void setFetchFieldAutoIncrement(boolean fetchFieldAutoIncrement) {
|
|
|
+ this.fetchFieldAutoIncrement = fetchFieldAutoIncrement;
|
|
|
+ }
|
|
|
+
|
|
|
public void setDialect(Dialect dialect) {
|
|
|
if (dialect != null) {
|
|
|
this.dialect = dialect;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 添加要生成的 tableName 到白名单
|
|
|
*/
|
|
|
@@ -94,13 +102,13 @@ public class MetaBuilder {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public void removeWhitelist(String tableName) {
|
|
|
if (tableName != null) {
|
|
|
this.whitelist.remove(tableName.trim());
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 添加要排除的 tableName 到黑名单
|
|
|
*/
|
|
|
@@ -115,17 +123,17 @@ public class MetaBuilder {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public void removeBlacklist(String tableName) {
|
|
|
if (tableName != null) {
|
|
|
this.blacklist.remove(tableName.trim());
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public void addExcludedTable(String... excludedTables) {
|
|
|
addBlacklist(excludedTables);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 设置需要被移除的表名前缀,仅用于生成 modelName 与 baseModelName
|
|
|
* 例如表名 "osc_account",移除前缀 "osc_" 后变为 "account"
|
|
|
@@ -133,19 +141,19 @@ public class MetaBuilder {
|
|
|
public void setRemovedTableNamePrefixes(String... removedTableNamePrefixes) {
|
|
|
this.removedTableNamePrefixes = removedTableNamePrefixes;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public void setTypeMapping(TypeMapping typeMapping) {
|
|
|
if (typeMapping != null) {
|
|
|
this.typeMapping = typeMapping;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public List<TableMeta> build() {
|
|
|
System.out.println("Build TableMeta ...");
|
|
|
try {
|
|
|
conn = dataSource.getConnection();
|
|
|
dbMeta = conn.getMetaData();
|
|
|
-
|
|
|
+
|
|
|
List<TableMeta> ret = new ArrayList<TableMeta>();
|
|
|
buildTableNames(ret);
|
|
|
for (TableMeta tableMeta : ret) {
|
|
|
@@ -164,7 +172,7 @@ public class MetaBuilder {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 移除没有主键的 table
|
|
|
protected void removeNoPrimaryKeyTable(List<TableMeta> ret) {
|
|
|
for (java.util.Iterator<TableMeta> it = ret.iterator(); it.hasNext();) {
|
|
|
@@ -188,13 +196,13 @@ public class MetaBuilder {
|
|
|
protected boolean isSkipTable(String tableName) {
|
|
|
return false;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 跳过不需要生成器处理的 table
|
|
|
- *
|
|
|
+ *
|
|
|
* 由于 setMetaBuilder 将置换掉 MetaBuilder,所以 Generator.addExcludedTable(...)
|
|
|
* 需要放在 setMetaBuilder 之后调用,否则 addExcludedTable 将无效
|
|
|
- *
|
|
|
+ *
|
|
|
* 示例:
|
|
|
Generator gen = new Generator(...);
|
|
|
gen.setMetaBuilder(new MetaBuilder(dataSource).skip(
|
|
|
@@ -204,13 +212,13 @@ public class MetaBuilder {
|
|
|
);
|
|
|
gen.addExcludedTable("error_log"); // 注意这行代码要放在上面的之后调用
|
|
|
gen.generate();
|
|
|
-
|
|
|
+
|
|
|
*/
|
|
|
public MetaBuilder skip(Predicate<String> tableSkip) {
|
|
|
this.tableSkip = tableSkip;
|
|
|
return this;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 构造 modelName,mysql 的 tableName 建议使用小写字母,多单词表名使用下划线分隔,不建议使用驼峰命名
|
|
|
* oracle 之下的 tableName 建议使用下划线分隔多单词名,无论 mysql还是 oralce,tableName 都不建议使用驼峰命名
|
|
|
@@ -225,22 +233,22 @@ public class MetaBuilder {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 将 oralce 大写的 tableName 转成小写,再生成 modelName
|
|
|
if (dialect.isOracle()) {
|
|
|
tableName = tableName.toLowerCase();
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return StrKit.firstCharToUpperCase(StrKit.toCamelCase(tableName));
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 使用 modelName 构建 baseModelName
|
|
|
*/
|
|
|
protected String buildBaseModelName(String modelName) {
|
|
|
return "Base" + modelName;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 不同数据库 dbMeta.getTables(...) 的 schemaPattern 参数意义不同
|
|
|
* 1:oracle 数据库这个参数代表 dbMeta.getUserName()
|
|
|
@@ -257,12 +265,12 @@ public class MetaBuilder {
|
|
|
return dbMeta.getTables(conn.getCatalog(), schemaPattern, null, new String[]{"TABLE"}); // 不支持 view 生成
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
protected void buildTableNames(List<TableMeta> ret) throws SQLException {
|
|
|
ResultSet rs = getTablesResultSet();
|
|
|
while (rs.next()) {
|
|
|
String tableName = rs.getString("TABLE_NAME");
|
|
|
-
|
|
|
+
|
|
|
// 如果使用白名单(size>0),则不在白名单之中的都将被过滤
|
|
|
if (whitelist.size() > 0 && !whitelist.contains(tableName)) {
|
|
|
System.out.println("Skip table :" + tableName);
|
|
|
@@ -273,68 +281,68 @@ public class MetaBuilder {
|
|
|
System.out.println("Skip table :" + tableName);
|
|
|
continue ;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// isSkipTable 为最早期的过滤机制,建议使用白名单、黑名单过滤
|
|
|
if (isSkipTable(tableName)) {
|
|
|
System.out.println("Skip table :" + tableName);
|
|
|
continue ;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// jfinal 4.3 新增过滤 table 机制
|
|
|
if (tableSkip != null && tableSkip.test(tableName)) {
|
|
|
System.out.println("Skip table :" + tableName);
|
|
|
continue ;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
TableMeta tableMeta = new TableMeta();
|
|
|
tableMeta.name = tableName;
|
|
|
tableMeta.remarks = rs.getString("REMARKS");
|
|
|
-
|
|
|
+
|
|
|
tableMeta.modelName = buildModelName(tableName);
|
|
|
tableMeta.baseModelName = buildBaseModelName(tableMeta.modelName);
|
|
|
ret.add(tableMeta);
|
|
|
}
|
|
|
rs.close();
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
protected void buildPrimaryKey(TableMeta tableMeta) throws SQLException {
|
|
|
ResultSet rs = dbMeta.getPrimaryKeys(conn.getCatalog(), null, tableMeta.name);
|
|
|
-
|
|
|
+
|
|
|
String primaryKey = "";
|
|
|
int index = 0;
|
|
|
while (rs.next()) {
|
|
|
String cn = rs.getString("COLUMN_NAME");
|
|
|
-
|
|
|
+
|
|
|
// 避免 oracle 驱动的 bug 生成重复主键,如:ID,ID
|
|
|
if (primaryKey.equals(cn)) {
|
|
|
continue ;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if (index++ > 0) {
|
|
|
primaryKey += ",";
|
|
|
}
|
|
|
primaryKey += cn;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 无主键的 table 将在后续的 removeNoPrimaryKeyTable() 中被移除,不再抛出异常
|
|
|
// if (StrKit.isBlank(primaryKey)) {
|
|
|
// throw new RuntimeException("primaryKey of table \"" + tableMeta.name + "\" required by active record pattern");
|
|
|
// }
|
|
|
-
|
|
|
+
|
|
|
tableMeta.primaryKey = primaryKey;
|
|
|
rs.close();
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 文档参考:
|
|
|
* http://dev.mysql.com/doc/connector-j/en/connector-j-reference-type-conversions.html
|
|
|
- *
|
|
|
+ *
|
|
|
* JDBC 与时间有关类型转换规则,mysql 类型到 java 类型如下对应关系:
|
|
|
* DATE java.sql.Date
|
|
|
* DATETIME java.sql.Timestamp
|
|
|
* TIMESTAMP[(M)] java.sql.Timestamp
|
|
|
* TIME java.sql.Time
|
|
|
- *
|
|
|
+ *
|
|
|
* 对数据库的 DATE、DATETIME、TIMESTAMP、TIME 四种类型注入 new java.util.Date()对象保存到库以后可以达到“秒精度”
|
|
|
* 为了便捷性,getter、setter 方法中对上述四种字段类型采用 java.util.Date,可通过定制 TypeMapping 改变此映射规则
|
|
|
*/
|
|
|
@@ -344,8 +352,8 @@ public class MetaBuilder {
|
|
|
ResultSet rs = stm.executeQuery(sql);
|
|
|
ResultSetMetaData rsmd = rs.getMetaData();
|
|
|
int columnCount = rsmd.getColumnCount();
|
|
|
-
|
|
|
-
|
|
|
+
|
|
|
+
|
|
|
Map<String, ColumnMeta> columnMetaMap = new HashMap<>();
|
|
|
if (generateRemarks) {
|
|
|
ResultSet colMetaRs = null;
|
|
|
@@ -365,13 +373,13 @@ public class MetaBuilder {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
-
|
|
|
+
|
|
|
+
|
|
|
for (int i=1; i<=columnCount; i++) {
|
|
|
ColumnMeta cm = new ColumnMeta();
|
|
|
// 备忘:getColumnName 获取字段真实名称而非 sql as 子句指定的名称
|
|
|
cm.name = rsmd.getColumnName(i);
|
|
|
-
|
|
|
+
|
|
|
String typeStr = null;
|
|
|
if (dialect.isKeepByteAndShort()) {
|
|
|
int type = rsmd.getColumnType(i);
|
|
|
@@ -381,12 +389,12 @@ public class MetaBuilder {
|
|
|
typeStr = "java.lang.Short";
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if (typeStr == null) {
|
|
|
String colClassName = rsmd.getColumnClassName(i);
|
|
|
typeStr = typeMapping.getType(colClassName);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if (typeStr == null) {
|
|
|
int type = rsmd.getColumnType(i);
|
|
|
if (type == Types.BINARY || type == Types.VARBINARY || type == Types.LONGVARBINARY || type == Types.BLOB) {
|
|
|
@@ -406,34 +414,39 @@ public class MetaBuilder {
|
|
|
typeStr = "java.lang.String";
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
typeStr = handleJavaType(typeStr, rsmd, i);
|
|
|
-
|
|
|
+
|
|
|
cm.javaType = typeStr;
|
|
|
-
|
|
|
+
|
|
|
// 构造字段对应的属性名 attrName
|
|
|
cm.attrName = buildAttrName(cm.name);
|
|
|
-
|
|
|
+
|
|
|
// 备注字段赋值
|
|
|
if (generateRemarks && columnMetaMap.containsKey(cm.name)) {
|
|
|
cm.remarks = columnMetaMap.get(cm.name).remarks;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ // 是否取出字段的自增属性
|
|
|
+ if (fetchFieldAutoIncrement) {
|
|
|
+ cm.isAutoIncrement = rsmd.isAutoIncrement(i);
|
|
|
+ }
|
|
|
+
|
|
|
tableMeta.columnMetas.add(cm);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
rs.close();
|
|
|
stm.close();
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* handleJavaType(...) 方法是用于处理 java 类型的回调方法,当 jfinal 默认
|
|
|
* 处理规则无法满足需求时,用户可以通过继承 MetaBuilder 并覆盖此方法定制自己的
|
|
|
* 类型转换规则
|
|
|
- *
|
|
|
+ *
|
|
|
* 当前实现只处理了 Oracle 数据库的 NUMBER 类型,根据精度与小数位数转换成 Integer、
|
|
|
* Long、BigDecimal。其它数据库直接返回原值 typeStr
|
|
|
- *
|
|
|
+ *
|
|
|
* Oracle 数据库 number 类型对应 java 类型:
|
|
|
* 1:如果不指定number的长度,或指定长度 n > 18
|
|
|
* number 对应 java.math.BigDecimal
|
|
|
@@ -441,7 +454,7 @@ public class MetaBuilder {
|
|
|
* number(n) 对应 java.lang.Long
|
|
|
* 3:如果number的长度在1 <= n <= 9
|
|
|
* number(n) 对应 java.lang.Integer 类型
|
|
|
- *
|
|
|
+ *
|
|
|
* 社区分享:《Oracle NUMBER 类型映射改进》https://jfinal.com/share/1145
|
|
|
*/
|
|
|
protected String handleJavaType(String typeStr, ResultSetMetaData rsmd, int column) throws SQLException {
|
|
|
@@ -449,7 +462,7 @@ public class MetaBuilder {
|
|
|
if ( ! dialect.isOracle() ) {
|
|
|
return typeStr;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 默认实现只处理 BigDecimal 类型
|
|
|
if ("java.math.BigDecimal".equals(typeStr)) {
|
|
|
int scale = rsmd.getScale(column); // 小数点右边的位数,值为 0 表示整数
|
|
|
@@ -467,10 +480,10 @@ public class MetaBuilder {
|
|
|
typeStr = "java.math.BigDecimal";
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return typeStr;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 构造 colName 所对应的 attrName,mysql 数据库建议使用小写字段名或者驼峰字段名
|
|
|
* Oralce 反射将得到大写字段名,所以不建议使用驼峰命名,建议使用下划线分隔单词命名法
|