自从上一次通过两个配置类解决flowable在人大金仓下的使用问题后,发现当我将配置文件中database-schema-update
配置项设置为true
时,生成表结构时会出现异常,于是就开始了这次的一顿操作。
根据异常堆栈和收集到的一些资料分析,生成表的过程中主要因为flowable默认使用liquibase来监控数据的版本变化,而很不巧它也不支持人大金仓,我甚至看了当前最高版本的liquibase,仍然是不支持的。因此只能自己扩展人大金仓的支持,人大金仓是基于postgres库的,因此,思路很简单,只要让liquibase认识人大金仓,并且让它按照处理postgres的过程来处理人大金仓就可以了。
这里直接说结果liquibase通过类加载器来加载所有liquibase.database.Database
的实现类,通过实现类的PRODUCT_NAME
来匹配到相应database处理器,用以支持相应数据库的方言处理。因此,我们需要实现一个KingBaseDatabase
,这里可以复制PostgresDatabase
类来进行改造,我直接把类放在下面
package liquibase.database.core;
import liquibase.CatalogAndSchema;
import liquibase.Scope;
import liquibase.changelog.column.LiquibaseColumn;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.DatabaseConnection;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.exception.DatabaseException;
import liquibase.logging.Logger;
import liquibase.statement.SqlStatement;
import liquibase.statement.core.RawCallStatement;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Index;
import liquibase.structure.core.Table;
import liquibase.util.StringUtil;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
/**
* 自定义的liquibase kingbase支持,该类基于liquibase4.3.5版本PostgresDatabase修改
* 注意:
* 1、该类不算稳定,可能存在潜在的问题
* 2、该类需要配合META-INF/services/liquibase.database.Database配置文件配合使用,否则该类无法被工厂扫描
* 3、注意该类需要同包路径覆盖
* @autr luoguohua
*/
public class KingBaseDatabase extends AbstractJdbcDatabase {
public static final String PRODUCT_NAME = "KingbaseES";
private static final int KINGBASE_DEFAULT_TCP_PORT_NUMBER = 54321;
private static final Logger LOG = Scope.getCurrentScope().getLog(KingBaseDatabase.class);
private Set systemTablesAndViews = new HashSet<>();
private Set reservedWords = new HashSet<>();
public KingBaseDatabase() {
super.setCurrentDateTimeFunction("NOW()");
// "Reserved" or "reserved (can be function or type)" in PostgreSQL
// from https://www.postgresql.org/docs/9.6/static/sql-keywords-appendix.html
reservedWords.addAll(Arrays.asList("ALL", "ANALYSE", "ANALYZE", "AND", "ANY", "ARRAY", "AS", "ASC",
"ASYMMETRIC", "AUTHORIZATION", "BINARY", "BOTH", "CASE", "CAST", "CHECK", "COLLATE", "COLLATION",
"COLUMN", "CONCURRENTLY", "CONSTRAINT", "CREATE", "CROSS", "CURRENT_CATALOG", "CURRENT_DATE",
"CURRENT_ROLE", "CURRENT_SCHEMA", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER", "DEFAULT",
"DEFERRABLE", "DESC", "DISTINCT", "DO", "ELSE", "END", "EXCEPT", "FALSE", "FETCH", "FOR", "FOREIGN",
"FREEZE", "FROM", "FULL", "GRANT", "GROUP", "HAVING", "ILIKE", "IN", "INITIALLY", "INNER", "INTERSECT",
"INTO", "IS", "ISNULL", "JOIN", "LATERAL", "LEADING", "LEFT", "LIKE", "LIMIT", "LOCALTIME",
"LOCALTIMESTAMP", "NATURAL", "NOT", "NOTNULL", "NULL", "OFFSET", "ON", "ONLY", "OR", "ORDER", "OUTER",
"OVERLAPS", "PLACING", "PRIMARY", "REFERENCES", "RETURNING", "RIGHT", "SELECT", "SESSION_USER",
"SIMILAR", "SOME", "SYMMETRIC", "TABLE", "TABLESAMPLE", "THEN", "TO", "TRAILING", "TRUE", "UNION",
"UNIQUE", "USER", "USING", "VARIADIC", "VERBOSE", "WHEN", "WHERE", "WINDOW", "WITH"));
super.sequenceNextValueFunction = "nextval('%s')";
super.sequenceCurrentValueFunction = "currval('%s')";
super.unmodifiableDataTypes.addAll(Arrays.asList("bool", "int4", "int8", "float4", "float8", "bigserial", "serial", "oid", "bytea", "date", "timestamptz", "text"));
super.unquotedObjectsAreUppercased=false;
}
@Override
public boolean equals(Object o) {
// Actually, we don't need and more specific checks than the base method. This exists just to make SONAR happy.
return super.equals(o);
}
@Override
public int hashCode() {
// Actually, we don't need and more specific hashing than the base method. This exists just to make SONAR happy.
return super.hashCode();
}
@Override
public Set getSystemViews() {
return systemTablesAndViews;
}
@Override
protected String getDefaultDatabaseProductName() {
return PRODUCT_NAME;
}
@Override // 该方法判断是否为liquibase实现的database
public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
return PRODUCT_NAME.equalsIgnoreCase(conn.getDatabaseProductName());
}
@Override
public String getDefaultDriver(String url) {
if (url.startsWith("jdbc:kingbase8:")) {
return "com.kingbase8.Driver";
}
return null;
}
@Override
public String getShortName() {
return "kingbase";
}
@Override
public Integer getDefaultPort() {
return KINGBASE_DEFAULT_TCP_PORT_NUMBER;
}
@Override
public boolean supportsInitiallyDeferrableColumns() {
return false;
}
@Override
public boolean supportsTablespaces() {
return false;
}
@Override
public int getPriority() {
return PRIORITY_DEFAULT;
}
@Override
public boolean supportsCatalogInObjectName(Class extends DatabaseObject> type) {
return false;
}
@Override
public boolean supportsSequences() {
return true;
}
@Override
public String getDatabaseChangeLogTableName() {
return super.getDatabaseChangeLogTableName().toLowerCase(Locale.US);
}
@Override
public String getDatabaseChangeLogLockTableName() {
return super.getDatabaseChangeLogLockTableName().toLowerCase(Locale.US);
}
@Override
public void setConnection(DatabaseConnection conn) {
super.setConnection(conn);
}
@Override
public boolean isSystemObject(DatabaseObject example) {
// All tables in the schemas pg_catalog and pg_toast are definitely system tables.
if
(
(example instanceof Table)
&& (example.getSchema() != null)
&& (
("SYS_CATALOG".equals(example.getSchema().getName()))
|| ("SYS_TOAST".equals(example.getSchema().getName()))
)
) {
return true;
}
return super.isSystemObject(example);
}
@Override
public String getAutoIncrementClause() {
return "";
}
@Override
public boolean generateAutoIncrementStartWith(BigInteger startWith) {
return false;
}
@Override
public boolean generateAutoIncrementBy(BigInteger incrementBy) {
return false;
}
@Override
public String escapeIndexName(final String catalogName, final String schemaName, final String indexName) {
return escapeObjectName(catalogName, schemaName, indexName, Index.class).toUpperCase().replace("PUBLIC.","");
}
@Override
public String escapeObjectName(String objectName, Class extends DatabaseObject> objectType) {
if ((quotingStrategy == ObjectQuotingStrategy.LEGACY) && hasMixedCase(objectName)) {
return "\"" + objectName + "\"";
} else if (objectType != null && LiquibaseColumn.class.isAssignableFrom(objectType)) {
return (objectName != null && !objectName.isEmpty()) ? objectName.trim() : objectName;
}
return super.escapeObjectName(objectName, objectType);
}
@Override
public String correctObjectName(String objectName, Class extends DatabaseObject> objectType) {
if ((objectName == null) || (quotingStrategy != ObjectQuotingStrategy.LEGACY)) {
return super.correctObjectName(objectName, objectType);
}
if (objectName.contains("-")
|| hasMixedCase(objectName)
|| startsWithNumeric(objectName)
|| isReservedWord(objectName)) {
return objectName;
} else {
return objectName.toLowerCase(Locale.US);
}
}
protected boolean hasMixedCase(String tableName) {
if (tableName == null) {
return false;
}
return StringUtil.hasUpperCase(tableName) && StringUtil.hasLowerCase(tableName);
}
@Override
public boolean isReservedWord(String tableName) {
return reservedWords.contains(tableName.toUpperCase());
}
@Override
protected SqlStatement getConnectionSchemaNameCallStatement() {
return new RawCallStatement("select current_schema()");
}
@Override
public String generatePrimaryKeyName(final String tableName) {
return tableName.toUpperCase(Locale.US) + "_PKEY";
}
@Override
public CatalogAndSchema.CatalogAndSchemaCase getSchemaAndCatalogCase() {
return CatalogAndSchema.CatalogAndSchemaCase.LOWER_CASE;
}
}
由于liquibase在扫描Database时,会扫描META-INF/services
目录下的liquibase.database.Database文件,因此我们需要覆盖该文件,并在文件中加上kingbase实现类的路径,否则无法被扫描到。
你以为这样就行了,还早的很呢,由于我们并没有对liquibase的所有涉及方言的部分进行改造,所以依然会存在一些问题,举例来说就是:**我们只是在办证的地方办了个假证,然后混进了哨站。**之后在执行各个行动的时候会遇到各种不匹配的地方,需要逐个调试。也就是需要针对不同的功能进行兼容。
第一个坑就是我们需要重写liquibase.datatype.core.DateTimeType
的datatime类的处理过程,因为在人大金仓里是没有datatime的。这里只需要在判断是postgres的database的if条件后面加上||database instanceof KingBaseDatabase
,就可以了,毕竟处理逻辑是一样。大部分情况下都可以套用这个思路。
第二个坑就是需要重写liquibase.snapshot.SnapshotGeneratorFactory
的has方法,在其异常抛出的时候人大金仓和postgres都需要rollback()。
上述两个类的重写都需要同包覆盖,完成后就可以在一个空库中正常生成flowable的表结构,但是后续发现在针对已经创建过数据结构的数据库的校验过程依然存在异常问题。这个问题留待后续有时间再解决。