/**
* Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jfinal.plugin.activerecord.generator;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.sql.DataSource;
import com.jfinal.kit.StrKit;
import com.jfinal.plugin.activerecord.dialect.Dialect;
import com.jfinal.plugin.activerecord.dialect.MysqlDialect;
import com.jfinal.plugin.activerecord.dialect.OracleDialect;
/**
* MetaBuilder
*/
public class MetaBuilder {
protected DataSource dataSource;
protected Dialect dialect = new MysqlDialect();
protected Set<String> excludedTables = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
protected Connection conn = null;
protected DatabaseMetaData dbMeta = null;
protected String[] removedTableNamePrefixes = null;
protected TypeMapping typeMapping = new TypeMapping();
public MetaBuilder(DataSource dataSource) {
if (dataSource == null) {
throw new IllegalArgumentException("dataSource can not be null.");
}
this.dataSource = dataSource;
}
public void setDialect(Dialect dialect) {
if (dialect != null) {
this.dialect = dialect;
}
}
public void addExcludedTable(String... excludedTables) {
if (excludedTables != null) {
for (String table : excludedTables) {
this.excludedTables.add(table);
}
}
}
/**
* 设置需要被移除的表名前缀,仅用于生成 modelName 与 baseModelName
* 例如表名 "osc_account",移除前缀 "osc_" 后变为 "account"
*/
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) {
buildPrimaryKey(tableMeta);
buildColumnMetas(tableMeta);
}
return ret;
}
catch (SQLException e) {
throw new RuntimeException(e);
}
finally {
if (conn != null) {
try {conn.close();} catch (SQLException e) {throw new RuntimeException(e);}
}
}
}
/**
* 通过继承并覆盖此方法,跳过一些不希望处理的 table,定制更加灵活的 table 过滤规则
* @return 返回 true 时将跳过当前 tableName 的处理
*/
protected boolean isSkipTable(String tableName) {
return false;
}
/**
* 构造 modelName,mysql 的 tableName 建议使用小写字母,多单词表名使用下划线分隔,不建议使用驼峰命名
* oracle 之下的 tableName 建议使用下划线分隔多单词名,无论 mysql还是 oralce,tableName 都不建议使用驼峰命名
*/
protected String buildModelName(String tableName) {
// 移除表名前缀仅用于生成 modelName、baseModelName,而 tableMeta.name 表名自身不能受影响
if (removedTableNamePrefixes != null) {
for (String prefix : removedTableNamePrefixes) {
if (tableName.startsWith(prefix)) {
tableName = tableName.replaceFirst(prefix, "");
break;
}
}
}
// 将 oralce 大写的 tableName 转成小写,再生成 modelName
if (dialect instanceof OracleDialect) {
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()
* 2:postgresql 数据库中需要在 jdbcUrl中配置 schemaPatter,例如:
* jdbc:postgresql://localhost:15432/djpt?currentSchema=public,sys,app
* 最后的参数就是搜索schema的顺序,DruidPlugin 下测试成功
* 3:开发者若在其它库中发现工作不正常,可通过继承 MetaBuilder并覆盖此方法来实现功能
*/
protected ResultSet getTablesResultSet() throws SQLException {
String schemaPattern = dialect instanceof OracleDialect ? dbMeta.getUserName() : null;
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");
if (excludedTables.contains(tableName)) {
System.out.println("Skip table :" + tableName);
continue ;
}
if (isSkipTable(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()) {
if (index++ > 0) {
primaryKey += ",";
}
primaryKey += rs.getString("COLUMN_NAME");
}
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 改变此映射规则
*/
protected void buildColumnMetas(TableMeta tableMeta) throws SQLException {
String sql = dialect.forTableBuilderDoBuild(tableMeta.name);
Statement stm = conn.createStatement();
ResultSet rs = stm.executeQuery(sql);
ResultSetMetaData rsmd = rs.getMetaData();
for (int i=1; i<=rsmd.getColumnCount(); i++) {
ColumnMeta cm = new ColumnMeta();
cm.name = rsmd.getColumnName(i);
String colClassName = rsmd.getColumnClassName(i);
String typeStr = typeMapping.getType(colClassName);
if (typeStr != null) {
cm.javaType = typeStr;
}
else {
int type = rsmd.getColumnType(i);
if (type == Types.BINARY || type == Types.VARBINARY || type == Types.BLOB) {
cm.javaType = "byte[]";
}
else if (type == Types.CLOB || type == Types.NCLOB) {
cm.javaType = "java.lang.String";
}
else {
cm.javaType = "java.lang.String";
}
}
// 构造字段对应的属性名 attrName
cm.attrName = buildAttrName(cm.name);
tableMeta.columnMetas.add(cm);
}
rs.close();
stm.close();
}
/**
* 构造 colName 所对应的 attrName,mysql 数据库建议使用小写字段名或者驼峰字段名
* Oralce 反射将得到大写字段名,所以不建议使用驼峰命名,建议使用下划线分隔单词命名法
*/
protected String buildAttrName(String colName) {
if (dialect instanceof OracleDialect) {
colName = colName.toLowerCase();
}
return StrKit.toCamelCase(colName);
}
}