feat: add a pluggable JDBC dialect layer with MySQL support
Fold Flyway V1-V7 into per-dialect baselines and resolve Boot 3/4 Flyway customizer types without a DataSource creation cycle. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -58,6 +58,14 @@
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.jetlumen.ordo.spring;
|
||||
|
||||
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.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.
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(prefix = "ordo", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@ConditionalOnClass({DataSource.class, Flyway.class})
|
||||
@ConditionalOnBean(DataSource.class)
|
||||
@AutoConfigureAfter(name = {
|
||||
"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",
|
||||
"org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration"
|
||||
})
|
||||
@AutoConfigureBefore(name = {
|
||||
"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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-11
@@ -19,6 +19,8 @@ import com.jetlumen.ordo.storage.jdbc.JdbcProcessDefinitionRepository;
|
||||
import com.jetlumen.ordo.storage.jdbc.JdbcProcessHistoryRepository;
|
||||
import com.jetlumen.ordo.storage.jdbc.JdbcProcessInstanceRepository;
|
||||
import com.jetlumen.ordo.storage.jdbc.JdbcTransactionExecutor;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
@@ -49,7 +51,8 @@ import java.time.Clock;
|
||||
"org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration",
|
||||
// Spring Boot 4.x locations (JDBC/Flyway autoconfiguration moved to dedicated modules)
|
||||
"org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration",
|
||||
"org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration"
|
||||
"org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration",
|
||||
"com.jetlumen.ordo.spring.OrdoFlywayAutoConfiguration"
|
||||
})
|
||||
@EnableConfigurationProperties(OrdoProperties.class)
|
||||
public class OrdoJdbcAutoConfiguration {
|
||||
@@ -78,6 +81,12 @@ public class OrdoJdbcAutoConfiguration {
|
||||
return ActionHandler.noop();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SqlDialect ordoSqlDialect(DataSource dataSource, OrdoProperties ordoProperties) {
|
||||
return SqlDialects.resolve(dataSource, ordoProperties.getJdbc().getDialect());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@DependsOnDatabaseInitialization
|
||||
@@ -93,32 +102,37 @@ public class OrdoJdbcAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ProcessDefinitionRepository ordoProcessDefinitionRepository(JdbcConnectionProvider connectionProvider) {
|
||||
return new JdbcProcessDefinitionRepository(connectionProvider);
|
||||
public ProcessDefinitionRepository ordoProcessDefinitionRepository(
|
||||
JdbcConnectionProvider connectionProvider, SqlDialect ordoSqlDialect) {
|
||||
return new JdbcProcessDefinitionRepository(connectionProvider, ordoSqlDialect);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ProcessInstanceRepository ordoProcessInstanceRepository(JdbcConnectionProvider connectionProvider) {
|
||||
return new JdbcProcessInstanceRepository(connectionProvider);
|
||||
public ProcessInstanceRepository ordoProcessInstanceRepository(
|
||||
JdbcConnectionProvider connectionProvider, SqlDialect ordoSqlDialect) {
|
||||
return new JdbcProcessInstanceRepository(connectionProvider, ordoSqlDialect);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ApprovalTaskRepository ordoApprovalTaskRepository(JdbcConnectionProvider connectionProvider) {
|
||||
return new JdbcApprovalTaskRepository(connectionProvider);
|
||||
public ApprovalTaskRepository ordoApprovalTaskRepository(
|
||||
JdbcConnectionProvider connectionProvider, SqlDialect ordoSqlDialect) {
|
||||
return new JdbcApprovalTaskRepository(connectionProvider, ordoSqlDialect);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ProcessHistoryRepository ordoProcessHistoryRepository(JdbcConnectionProvider connectionProvider) {
|
||||
return new JdbcProcessHistoryRepository(connectionProvider);
|
||||
public ProcessHistoryRepository ordoProcessHistoryRepository(
|
||||
JdbcConnectionProvider connectionProvider, SqlDialect ordoSqlDialect) {
|
||||
return new JdbcProcessHistoryRepository(connectionProvider, ordoSqlDialect);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ActionExecutionRepository ordoActionExecutionRepository(JdbcConnectionProvider connectionProvider) {
|
||||
return new JdbcActionExecutionRepository(connectionProvider);
|
||||
public ActionExecutionRepository ordoActionExecutionRepository(
|
||||
JdbcConnectionProvider connectionProvider, SqlDialect ordoSqlDialect) {
|
||||
return new JdbcActionExecutionRepository(connectionProvider, ordoSqlDialect);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
+18
@@ -11,6 +11,7 @@ public class OrdoProperties {
|
||||
|
||||
private final Definitions definitions = new Definitions();
|
||||
private final Due due = new Due();
|
||||
private final Jdbc jdbc = new Jdbc();
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
@@ -28,6 +29,10 @@ public class OrdoProperties {
|
||||
return due;
|
||||
}
|
||||
|
||||
public Jdbc getJdbc() {
|
||||
return jdbc;
|
||||
}
|
||||
|
||||
public static class Definitions {
|
||||
private String location = "classpath*:ordo/*.json";
|
||||
|
||||
@@ -52,4 +57,17 @@ public class OrdoProperties {
|
||||
this.pollMs = pollMs;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Jdbc {
|
||||
/** Explicit dialect id ({@code postgresql}, {@code mysql}). Empty means detect from the DataSource. */
|
||||
private String dialect;
|
||||
|
||||
public String getDialect() {
|
||||
return dialect;
|
||||
}
|
||||
|
||||
public void setDialect(String dialect) {
|
||||
this.dialect = dialect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -1 +1,2 @@
|
||||
com.jetlumen.ordo.spring.OrdoFlywayAutoConfiguration
|
||||
com.jetlumen.ordo.spring.OrdoJdbcAutoConfiguration
|
||||
|
||||
+10
-1
@@ -13,6 +13,7 @@ import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
|
||||
@@ -34,7 +35,8 @@ class OrdoJdbcAutoConfigurationTest {
|
||||
|
||||
private final ApplicationContextRunner withDataSourceRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(
|
||||
DataSourceAutoConfiguration.class, FlywayAutoConfiguration.class, OrdoJdbcAutoConfiguration.class))
|
||||
DataSourceAutoConfiguration.class, OrdoFlywayAutoConfiguration.class,
|
||||
FlywayAutoConfiguration.class, OrdoJdbcAutoConfiguration.class))
|
||||
.withPropertyValues(
|
||||
"spring.datasource.url=jdbc:h2:mem:ordo_" + UUID.randomUUID()
|
||||
+ ";MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1",
|
||||
@@ -47,6 +49,7 @@ class OrdoJdbcAutoConfigurationTest {
|
||||
void assemblesJdbcBackedEngineWhenDataSourceIsPresent() {
|
||||
withDataSourceRunner.run(context -> {
|
||||
assertThat(context).hasSingleBean(OrdoEngine.class);
|
||||
assertThat(context.getBean(SqlDialect.class).id()).isEqualTo("postgresql");
|
||||
OrdoEngine engine = context.getBean(OrdoEngine.class);
|
||||
|
||||
engine.publish(LEAVE_REQUEST);
|
||||
@@ -60,6 +63,12 @@ class OrdoJdbcAutoConfigurationTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void honoursExplicitMysqlDialect() {
|
||||
withDataSourceRunner.withPropertyValues("ordo.jdbc.dialect=mysql", "spring.flyway.enabled=false")
|
||||
.run(context -> assertThat(context.getBean(SqlDialect.class).id()).isEqualTo("mysql"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void backsOffWhenNoDataSourceBeanIsPresent() {
|
||||
withoutDataSourceRunner.run(context ->
|
||||
|
||||
Reference in New Issue
Block a user