feat: migrate Ordo schema with a dedicated Flyway history table
Stop rewriting spring.flyway.locations so host migrations stay independent and Boot 4 no longer needs starter-flyway for Ordo tables. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
| `ordo-spring-boot-starter` | Spring Boot 自动装配(JDBC + Flyway) |
|
||||
| `ordo-example` | 内存引擎示例 |
|
||||
|
||||
存储实现通过 dialect 层支持 **PostgreSQL** 与 **MySQL**(测试可用 H2 PostgreSQL 兼容模式)。Starter **不携带** JDBC 驱动,宿主自行加入 `postgresql`、`mysql-connector-j` 或 `h2`。Spring Boot 4 还需额外引入 `spring-boot-starter-flyway`,否则迁移不会跑。
|
||||
存储实现通过 dialect 层支持 **PostgreSQL** 与 **MySQL**(测试可用 H2 PostgreSQL 兼容模式)。Starter **不携带** JDBC 驱动,宿主自行加入 `postgresql`、`mysql-connector-j` 或 `h2`。表结构由 Ordo 自己的 Flyway 迁移(`ordo_schema_history`),与宿主 `spring.flyway` 无关。
|
||||
|
||||
## 能力
|
||||
|
||||
|
||||
+4
-2
@@ -34,7 +34,7 @@ Java 包(模块未变):
|
||||
| `com.jetlumen.ordo.api.exception` / `query` / `repository` | 异常、分页查询、存储端口 |
|
||||
| `com.jetlumen.ordo.core.spi` | `DispatchingActionHandler`、`DispatchingRoutingCondition`、`RegistryOrdoCatalog` |
|
||||
|
||||
Starter **不携带** JDBC 驱动。生产按库添加 `org.postgresql:postgresql` 或 `com.mysql:mysql-connector-j`。Spring Boot 4 还需 `spring-boot-starter-flyway`,否则 Flyway 迁移不会执行。
|
||||
Starter **不携带** JDBC 驱动。生产按库添加 `org.postgresql:postgresql` 或 `com.mysql:mysql-connector-j`。Ordo 用独立 Flyway 建表,不依赖宿主 `spring.flyway`。
|
||||
|
||||
先 `mvn install` 本仓库,宿主再依赖 `0.0.1-SNAPSHOT`。
|
||||
|
||||
@@ -72,6 +72,8 @@ ordo:
|
||||
enabled: true
|
||||
jdbc:
|
||||
dialect: # 可选 postgresql / mysql;空则按 DataSource 探测
|
||||
migrate: true
|
||||
history-table: ordo_schema_history
|
||||
definitions:
|
||||
location: classpath*:ordo/*.json # 启动时对每个 JSON 调用 publish
|
||||
due:
|
||||
@@ -420,7 +422,7 @@ ACTION 成功事件发生在提交之后,因此排在同轮事务内写入的
|
||||
|
||||
## 12. 存储
|
||||
|
||||
Flyway 脚本按方言分目录:`db/postgresql/migration`、`db/mysql/migration`。未设置 `spring.flyway.locations` 时,starter 按探测到的方言指向对应目录。已有 `flyway_schema_history` 的开发库按版本追加迁移(PARALLEL 为 `V3__parallel_blocks.sql`)。表包括流程头 `ordo_process`、按 `(id, version)` 存储的定义/步骤(含 `parent_step_id` / `branch_id`)/候选人/转移、实例(含 `definition_version`)、任务、并行令牌 `ordo_instance_token`、`ordo_process_event`、`ordo_action_execution`。
|
||||
Flyway 脚本按方言分目录:`db/postgresql/migration`、`db/mysql/migration`。Starter 在启动时用独立 Flyway 执行这些脚本,历史表默认 `ordo_schema_history`,不修改 `spring.flyway.locations`。若宿主也启用了 Spring Boot Flyway,Ordo 会在宿主 `flywayInitializer` 之后再 migrate,避免非空 schema 导致宿主失败。已有 `ordo_*` 表但尚无该历史表时,会 baseline 到当前脚本最高版本后再 migrate。`ordo.jdbc.migrate=false` 时不执行。表包括流程头 `ordo_process`、按 `(id, version)` 存储的定义/步骤(含 `parent_step_id` / `branch_id`)/候选人/转移、实例(含 `definition_version`)、任务、并行令牌 `ordo_instance_token`、`ordo_process_event`、`ordo_action_execution`。
|
||||
|
||||
自定义方言:实现 `SqlDialect` 并用 `META-INF/services` 注册,或提供 `SqlDialect` Bean。新增列时每个已支持方言目录各加一条迁移。
|
||||
|
||||
|
||||
+23
@@ -78,6 +78,12 @@ public class OrdoProperties {
|
||||
/** Explicit dialect id ({@code postgresql}, {@code mysql}). Empty means detect from the DataSource. */
|
||||
private String dialect;
|
||||
|
||||
/** When true, Ordo runs its own Flyway against dialect locations. */
|
||||
private boolean migrate = true;
|
||||
|
||||
/** Flyway history table used only by Ordo (not {@code flyway_schema_history}). */
|
||||
private String historyTable = "ordo_schema_history";
|
||||
|
||||
public String getDialect() {
|
||||
return dialect;
|
||||
}
|
||||
@@ -85,6 +91,23 @@ public class OrdoProperties {
|
||||
public void setDialect(String dialect) {
|
||||
this.dialect = dialect;
|
||||
}
|
||||
|
||||
public boolean isMigrate() {
|
||||
return migrate;
|
||||
}
|
||||
|
||||
public void setMigrate(boolean migrate) {
|
||||
this.migrate = migrate;
|
||||
}
|
||||
|
||||
public String getHistoryTable() {
|
||||
return historyTable;
|
||||
}
|
||||
|
||||
public void setHistoryTable(String historyTable) {
|
||||
this.historyTable = historyTable == null || historyTable.isBlank()
|
||||
? "ordo_schema_history" : historyTable.trim();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Rest {
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDatabaseInitializerDetector;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/** Treats {@link OrdoSchemaMigrator} as database initialization for {@code @DependsOnDatabaseInitialization}. */
|
||||
public class OrdoDatabaseInitializerDetector extends AbstractBeansOfTypeDatabaseInitializerDetector {
|
||||
|
||||
@Override
|
||||
protected Set<Class<?>> getDatabaseInitializerBeanTypes() {
|
||||
return Set.of(OrdoSchemaMigrator.class);
|
||||
}
|
||||
}
|
||||
+10
-92
@@ -1,34 +1,22 @@
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.api.configuration.FluentConfiguration;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
/**
|
||||
* Points Flyway at the dialect migration directory before Flyway runs.
|
||||
* Host {@code spring.flyway.locations} is left unchanged when set.
|
||||
*
|
||||
* <p>Does not inject {@link DataSource} or {@link SqlDialect} at bean-creation time
|
||||
* (that would cycle with DataSource → Flyway → customizer). Dialect is resolved inside
|
||||
* {@code customize} from the FluentConfiguration DataSource / {@code ordo.jdbc.dialect}.
|
||||
*
|
||||
* <p>FlywayConfigurationCustomizer moved between Boot 3 and Boot 4; a reflective
|
||||
* {@link FactoryBean} supplies a proxy for whichever type is on the classpath.
|
||||
* Migrates Ordo tables with a dedicated Flyway history table, independent of
|
||||
* {@code spring.flyway}. Runs after the host {@code flywayInitializer} when present so a
|
||||
* non-empty schema does not break host migrate.
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(prefix = "ordo", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@@ -36,89 +24,19 @@ import java.lang.reflect.Proxy;
|
||||
@ConditionalOnBean(DataSource.class)
|
||||
@AutoConfigureAfter(name = {
|
||||
"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",
|
||||
"org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration"
|
||||
})
|
||||
@AutoConfigureBefore(name = {
|
||||
"org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration",
|
||||
"org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration",
|
||||
"org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration"
|
||||
})
|
||||
@EnableConfigurationProperties(OrdoProperties.class)
|
||||
public class OrdoFlywayAutoConfiguration {
|
||||
|
||||
private static final String[] FLYWAY_CUSTOMIZER_TYPES = {
|
||||
"org.springframework.boot.flyway.autoconfigure.FlywayConfigurationCustomizer",
|
||||
"org.springframework.boot.autoconfigure.flyway.FlywayConfigurationCustomizer"
|
||||
};
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.flywaydb.core.api.configuration.FluentConfiguration")
|
||||
public FactoryBean<Object> ordoFlywayConfigurationCustomizer(
|
||||
OrdoProperties ordoProperties, Environment environment) {
|
||||
Class<?> customizerType = resolveFlywayCustomizerType();
|
||||
if (customizerType == null) {
|
||||
return null;
|
||||
}
|
||||
return new FlywayLocationsCustomizerFactoryBean(customizerType, ordoProperties, environment);
|
||||
}
|
||||
|
||||
static Class<?> resolveFlywayCustomizerType() {
|
||||
ClassLoader classLoader = OrdoFlywayAutoConfiguration.class.getClassLoader();
|
||||
for (String name : FLYWAY_CUSTOMIZER_TYPES) {
|
||||
try {
|
||||
return Class.forName(name, false, classLoader);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// Boot 3 vs Boot 4
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class FlywayLocationsCustomizerFactoryBean implements FactoryBean<Object> {
|
||||
private final Class<?> customizerType;
|
||||
private final OrdoProperties ordoProperties;
|
||||
private final Environment environment;
|
||||
|
||||
private FlywayLocationsCustomizerFactoryBean(Class<?> customizerType, OrdoProperties ordoProperties,
|
||||
Environment environment) {
|
||||
this.customizerType = customizerType;
|
||||
this.ordoProperties = ordoProperties;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getObject() {
|
||||
return Proxy.newProxyInstance(customizerType.getClassLoader(), new Class<?>[] {customizerType},
|
||||
(proxy, method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("customize".equals(name) && args != null && args.length == 1) {
|
||||
applyLocations((FluentConfiguration) args[0]);
|
||||
return null;
|
||||
}
|
||||
if ("equals".equals(name)) {
|
||||
return proxy == args[0];
|
||||
}
|
||||
if ("hashCode".equals(name)) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if ("toString".equals(name)) {
|
||||
return "OrdoFlywayLocationsCustomizer";
|
||||
}
|
||||
throw new UnsupportedOperationException(method.toString());
|
||||
});
|
||||
}
|
||||
|
||||
private void applyLocations(FluentConfiguration configuration) {
|
||||
if (environment.containsProperty("spring.flyway.locations")) {
|
||||
return;
|
||||
}
|
||||
SqlDialect dialect = SqlDialects.resolve(configuration.getDataSource(),
|
||||
ordoProperties.getJdbc().getDialect());
|
||||
configuration.locations(dialect.flywayLocations());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return customizerType;
|
||||
public OrdoSchemaMigrator ordoSchemaMigrator(DataSource dataSource, OrdoProperties ordoProperties,
|
||||
BeanFactory beanFactory) {
|
||||
if (beanFactory.containsBean("flywayInitializer")) {
|
||||
beanFactory.getBean("flywayInitializer");
|
||||
}
|
||||
return new OrdoSchemaMigrator(dataSource, ordoProperties);
|
||||
}
|
||||
}
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.api.MigrationInfo;
|
||||
import org.flywaydb.core.api.MigrationVersion;
|
||||
import org.flywaydb.core.api.configuration.FluentConfiguration;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Runs Ordo Flyway scripts independently of the host {@code spring.flyway} instance.
|
||||
* Presence of this bean marks Ordo schema initialization for
|
||||
* {@code @DependsOnDatabaseInitialization}.
|
||||
*/
|
||||
public final class OrdoSchemaMigrator {
|
||||
|
||||
private static final String PROCESS_TABLE = "ordo_process";
|
||||
|
||||
public OrdoSchemaMigrator(DataSource dataSource, OrdoProperties properties) {
|
||||
OrdoProperties.Jdbc jdbc = properties.getJdbc();
|
||||
if (!jdbc.isMigrate()) {
|
||||
return;
|
||||
}
|
||||
String[] locations = SqlDialects.resolve(dataSource, jdbc.getDialect()).flywayLocations();
|
||||
String historyTable = jdbc.getHistoryTable();
|
||||
FluentConfiguration configuration = new FluentConfiguration(OrdoSchemaMigrator.class.getClassLoader())
|
||||
.dataSource(dataSource)
|
||||
.locations(locations)
|
||||
.table(historyTable);
|
||||
applyBaselineIfNeeded(configuration, dataSource, historyTable);
|
||||
configuration.load().migrate();
|
||||
}
|
||||
|
||||
private static void applyBaselineIfNeeded(FluentConfiguration configuration, DataSource dataSource,
|
||||
String historyTable) {
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
if (tableExists(metaData, connection, historyTable)) {
|
||||
return;
|
||||
}
|
||||
// Host tables / flyway_schema_history make the schema non-empty; Flyway then requires
|
||||
// baseline before migrate. Existing Ordo tables baseline to latest (skip scripts);
|
||||
// otherwise baseline at 0 so V1+ still run.
|
||||
if (tableExists(metaData, connection, PROCESS_TABLE)) {
|
||||
MigrationVersion latest = latestVersion(configuration.load());
|
||||
configuration.baselineOnMigrate(true).baselineVersion(latest);
|
||||
} else {
|
||||
configuration.baselineOnMigrate(true).baselineVersion(MigrationVersion.fromVersion("0"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("failed to inspect schema before Ordo Flyway migrate", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean tableExists(DatabaseMetaData metaData, Connection connection, String table)
|
||||
throws SQLException {
|
||||
String catalog = connection.getCatalog();
|
||||
String schema = connection.getSchema();
|
||||
String[] names = {table, table.toLowerCase(Locale.ROOT), table.toUpperCase(Locale.ROOT)};
|
||||
for (String name : names) {
|
||||
try (ResultSet tables = metaData.getTables(catalog, schema, name, new String[] {"TABLE"})) {
|
||||
if (tables.next()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static MigrationVersion latestVersion(Flyway flyway) {
|
||||
MigrationVersion latest = MigrationVersion.fromVersion("0");
|
||||
for (MigrationInfo info : flyway.info().all()) {
|
||||
if (info.getVersion() != null && info.getVersion().compareTo(latest) > 0) {
|
||||
latest = info.getVersion();
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.jetlumen.ordo.spring.jdbc.OrdoDatabaseInitializerDetector
|
||||
+1
-1
@@ -73,7 +73,7 @@ class OrdoJdbcAutoConfigurationTest {
|
||||
|
||||
@Test
|
||||
void honoursExplicitMysqlDialect() {
|
||||
withDataSourceRunner.withPropertyValues("ordo.jdbc.dialect=mysql", "spring.flyway.enabled=false")
|
||||
withDataSourceRunner.withPropertyValues("ordo.jdbc.dialect=mysql", "ordo.jdbc.migrate=false")
|
||||
.run(context -> assertThat(context.getBean(SqlDialect.class).id()).isEqualTo("mysql"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user