Flowable 与 nullDatabaseMeansCurrent=true

2024-09-30, 星期一, 17:09

Troubleshooting故事会

一个 Flowable 项目在启动时抛出表不存在的错误。

这是一个 Spring Boot 3 项目,使用了 Flowable 7.0.1,出现问题的是测试环境的数据库,MySQL 5.7.41。

很奇怪,因为默认配置下 Flowable 启动时会检查数据库中是否存在相关的表,没有则会创建。搜索一番,有人提到在 JDBC 连接参数中加上 nullCatalogMeansCurrent=true 可解,在 MySQL Connector/J 8.0 中这个参数的默认值改成了 false

3.3.4.1 Upgrading to MySQL Connector/J 8.0 from Connector/J 5.1 提到了这次修改,此外在一些发布日志(例如 Changes in MySQL Connector/J 8.0.17 (2019-07-22, General Availability))中也可以看到 nullCatalogMeansCurrent 这个参数重新命名成了 nullDatabaseMeansCurrent

MySQL Connector/J Developer Guide 中提到这个参数的作用是 In ‘DatabaseMetaData’ methods that take a ‘catalog’ or ‘schema’ parameter, does the value “null” mean to use the current database?

已经在配置中显示指定了数据源的情况下为什么要特别说明连接哪个数据库?通过调试看看发生了什么。

执行逻辑是在 org.flowable.common.engine.impl.db.ServiceSqlScriptBasedDbSchemaManager#schemaUpdate(java.lang.String) 方法中分道扬镳的:

// ...
if (isUpdateNeeded()) {
    // nullDatabaseMeansCurrent=false
    dbSchemaUpgrade(...
} else {
    // nullDatabaseMeansCurrent=true
    schemaCreate();
}
// ...

isUpdateNeeded() 通过 org.flowable.common.engine.impl.db.AbstractSqlScriptBasedDbSchemaManager#isTablePresent(java.lang.String) 判断给定表是否存在,判断依据是下方的查询结果集是否有匹配:

ResultSet tables = databaseMetaData.getTables(catalog, schema, tableName, JDBC_METADATA_TABLE_TYPES);

不过无论代码执行是否异常,catalogschema 始终为 null,看不出区别。继续调试,项目使用了 HikariCP,调用链如下:

com.zaxxer.hikari.pool.ProxyDatabaseMetaData#getTables
|-- com.mysql.cj.jdbc.DatabaseMetaData#getTables

后者使用 getDatabase 方法获取数据库的名称,用于查询表:

protected String getDatabase(String catalog, String schema) {
    if (this.databaseTerm.getValue() == DatabaseTerm.SCHEMA) {
        return schema == null && this.nullDatabaseMeansCurrent.getValue() ? this.database : schema;
    }
    return catalog == null && this.nullDatabaseMeansCurrent.getValue() ? this.database : catalog;
}

nullDatabaseMeansCurrent=true 时返回当前数据库,若 nullDatabaseMeansCurrent=false,由于 catalogschema 均为 nullgetTables 会搜索整个 MySQL 实例上的表。刚好这个测试 MySQL 实例上其他数据库中存在 Flowable 相关的表,删除之后项目恢复正常。

不过其他场景下就不能如此随便了,因此另一个方案是填上 catalog / schema 参数。

分析代码,在org.flowable.common.engine.impl.db.AbstractSqlScriptBasedDbSchemaManager#isTablePresent(java.lang.String) 中设置了 catalog 变量。

String catalog = dbSqlSession.getConnectionMetadataDefaultCatalog();
if (dbSqlSessionFactory.getDatabaseCatalog() != null && dbSqlSessionFactory.getDatabaseCatalog().length() > 0) {
    catalog = dbSqlSessionFactory.getDatabaseCatalog();
}

由于这个 org.flowable.common.engine.impl.db.DbSqlSession 实例既没有在构造时为 connectionMetadataDefaultCatalog 赋值,也没有在后续调用 Setter 方法,因此 catalog 的值只能来自 org.flowable.common.engine.impl.db.DbSqlSessionFactory 实例的 databaseCatalog 属性。

org.flowable.common.engine.impl.AbstractEngineConfiguration#initDbSqlSessionFactory 中调用了这些属性的 Setter 方法,在 FlowableEngineConfigurationConfigurer 中补充配置,项目可以正常运行了。

@Configuration
public class FlowableEngineConfigurationConfigurer
    implements EngineConfigurationConfigurer<SpringProcessEngineConfiguration> {
    public void configure(SpringProcessEngineConfiguration processEngineConfiguration) {
        processEngineConfiguration
            // ...
            .setDatabaseCatalog("dorian");
}

schema 同理,不过若要使用 setDatabaseSchema("dorian"),还需在 JDBC URL 连接中添加 databaseTerm=SCHEMA

MySQL uses the term “schema” as a synonym of the term “database,” while Connector/J historically takes the JDBC term “catalog” as synonymous to “database”. This property sets for Connector/J which of the JDBC terms “catalog” and “schema” is used in an application to refer to a database. The property takes one of the two values “CATALOG” or “SCHEMA” and uses it to determine (1) which Connection methods can be used to set/get the current database (e.g. ‘setCatalog()’ or ‘setSchema()’?), (2) which arguments can be used within the various ‘DatabaseMetaData’ methods to filter results (e.g. the catalog or ‘schemaPattern’ argument of ‘getColumns()’?), and (3) which fields in the result sets returned by ‘DatabaseMetaData’ methods contain the database identification information (i.e., the ‘TABLE_CAT’ or ‘TABLE_SCHEM’ field in the result set returned by ‘getTables()’?).

If “databaseTerm=CATALOG”, ‘schemaPattern’ for searches are ignored and calls of schema methods (like ‘setSchema()’ or get ‘Schema()’) become no-ops, and vice versa.

顺便看了下 Relationship between catalog, schema, user, and database instance 提到的在 Postgres 等数据库中这些概念的区别。