fix: EXPOSED-1004 SpringBoot-Starter Run DatabaseInitializer DDL during bean initialization - #2820
Conversation
…ase with SPI ordering DatabaseInitializer was implemented as ApplicationRunner, which executes after the Spring context has fully refreshed. This caused a race condition where beans with @PostConstruct or InitializingBean that query the database would fail with "table not found" errors because DDL had not yet been executed. Changes: - Replace ApplicationRunner with InitializingBean so DDL runs during bean creation via afterPropertiesSet() with TransactionTemplate - Add ExposedDatabaseInitializerDetector (DatabaseInitializerDetector SPI) registered in META-INF/spring.factories so Spring Boot automatically orders dependent beans (JdbcOperations, @DependsOnDatabaseInitialization) after schema creation without requiring explicit @dependsOn - Update ExposedAutoConfiguration to inject SpringTransactionManager into DatabaseInitializer for programmatic transaction management - Add DatabaseInitializerEarlyInitTest verifying SPI-based auto ordering
…ring-boot4 starter Apply the same race-condition fix that was made to exposed-spring-boot-starter to exposed-spring-boot4-starter, so Spring Boot 4 users also get schema-ready beans during their initialization phase instead of after context refresh. - DatabaseInitializer: ApplicationRunner -> InitializingBean. Constructor now takes PlatformTransactionManager; DDL runs in afterPropertiesSet() via TransactionTemplate (no longer relies on @Transactional/AOP proxy, which is not active during bean initialization). - ExposedAutoConfiguration.databaseInitializer() now injects SpringTransactionManager. - New ExposedDatabaseInitializerDetector implementing Spring Boot's DatabaseInitializerDetector SPI. - New META-INF/spring.factories registering the detector so that @DependsOnDatabaseInitialization beans are automatically ordered after DDL. - DatabaseInitializerTest updated for the new constructor signature. - New DatabaseInitializerEarlyInitTest verifying SPI-based automatic ordering works under Spring Boot 4. Verified against Spring Boot 4.0.0 with H2.
…gBean pattern The Spring Boot integration guide and the exposed-spring sample previously recommended ApplicationRunner + @transactional for manual schema creation (used as the GraalVM native-image workaround and as a standalone sample). That pattern carries the same race condition that EXPOSED-1004 fixes: DDL runs after context refresh, so beans whose @PostConstruct or afterPropertiesSet touches the database fail with "table not found". Update both to InitializingBean + TransactionTemplate so users following the docs / sample get schema-ready beans during their initialization phase, consistent with the auto-configuration starter behavior. - documentation-website/Writerside/topics/Spring-Boot-integration.md: - "Enable automatic schema creation" section now mentions that DDL runs during the bean initialization phase and points to @DependsOnDatabaseInitialization for ordering. - AOT workaround example switched to InitializingBean + TransactionTemplate, with a note explaining why @transactional doesn't work during afterPropertiesSet(). - samples/exposed-spring/.../SchemaInitialize.kt: - Same pattern migration.
| @@ -0,0 +1,2 @@ | |||
| org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\ | |||
| org.jetbrains.exposed.v1.spring.boot4.autoconfigure.ExposedDatabaseInitializerDetector | |||
There was a problem hiding this comment.
| Item | Detail |
|---|---|
| What | Register the DatabaseInitializerDetector implementation in spring.factories |
| Why | Spring decides ordering in a phase (BeanFactoryPostProcessor) that runs before beans are created, where bean injection isn't possible |
| How | Loaded via SPI (SpringFactoriesLoader) straight from the classpath, without the context |
| If registered as a bean instead | It wouldn't be discovered during the ordering phase → @DependsOnDatabaseInitialization ordering would silently break |
| @Transactional | ||
| override fun run(args: ApplicationArguments) { | ||
| override fun afterPropertiesSet() { | ||
| TransactionTemplate(transactionManager).execute { |
There was a problem hiding this comment.
Moving to TransactionTemplate is mandatory, not optional.
The AOP proxy isn't active during afterPropertiesSet(), so Transactional gets silently ignored.
If this was overlooked, the DDL likely executed without a transaction or failed.
| * [org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization] are initialized after the | ||
| * schema has been created by [DatabaseInitializer.afterPropertiesSet]. | ||
| */ | ||
| class ExposedDatabaseInitializerDetector : DatabaseInitializerDetector { |
There was a problem hiding this comment.
By default, DatabaseInitializer initializes after user beans because auto-config beans are registered later.
To fix this, we register ExposedDatabaseInitializerDetector in spring.factories.
Spring Boot's DependsOnDatabaseInitializationPostProcessor then adds a dependsOn edge to the detected beans, forcing the initializer to load earlier.
Therefore, the SPI registration is the key part of this PR—switching to InitializingBean alone wouldn't fix it.
The new test validates this exact mechanism: without the detector, the verifier bean initializes before the initializer and fails."
Description
Since my English is a bit rusty, I wrote this description with a little help from Gemini.
Following the successful merge of the previous PR, I'm happy to submit this next one!
Following up on the previously merged PR, this change moves DDL execution from an
ApplicationRunner(which runs after context refresh) to the bean initialization phase (InitializingBean), and registers aDatabaseInitializerDetectorviaspring.factoriesso Spring Boot automatically runs schema creation before any bean annotated with@DependsOnDatabaseInitialization. The same change is applied to both the Spring Boot 3 and 4 starter modules.The problem
The current
DatabaseInitializeris anApplicationRunner, which Spring Boot invokes after the context has finished refreshing. That's too late for beans that need the schema during their own initialization:Even with
spring.exposed.generate-ddl=true, the app fails to boot, and the user gets a generic SQL error with no hint that DDL hasn't run yet.This is unlike JPA, where
LocalContainerEntityManagerFactoryBeanrunsddl-autoinside its ownafterPropertiesSet(), so dependent beans always find the schema ready.The fix
Two changes, both standard Spring Boot patterns:
1.
DatabaseInitializeris now anInitializingBean.DDL runs inside
afterPropertiesSet(), wrapped in aTransactionTemplate. This is the same lifecycle phase as@PostConstruct, so any bean ordered afterDatabaseInitializeris guaranteed a ready schema. (TransactionTemplateis used instead of@Transactionalbecause the AOP proxy behind the annotation isn't active yet during bean initialization.)2. A
DatabaseInitializerDetectoris registered viaspring.factories.Spring Boot's
DatabaseInitializationDependencyConfigurerpicks it up and adds adependsOnedge to every bean annotated with@DependsOnDatabaseInitialization— no manual@DependsOnwiring needed. This is the same SPI that Flyway and Liquibase use, so Exposed composes naturally with them. Verified thatspring.factoriesloading works in both Spring Boot 3 and 4.What does this mean for users?
For most users: nothing changes.
If you have
spring.exposed.generate-ddl=trueand your beans access the database only after startup (regular@Service,@Controller, etc.), everything keeps working — DDL just runs slightly earlier.For beans that need the schema during their own initialization:
Add
@DependsOnDatabaseInitializationto the bean:Beans that depend on
JdbcOperations/JdbcTemplate/JdbcClientget this for free — Spring Boot's built-in detector handles them.Breaking change
The
DatabaseInitializerconstructor now requires aPlatformTransactionManager:This affects users who subclass or directly instantiate
DatabaseInitializer. Users of the auto-configured bean are unaffected.Alternative considered: PR #2762
#2762 targets the same issue by running DDL inside
Database.connect()via a newDatabaseConfig.ddlfield inexposed-core. This PR instead keeps the change scoped to the two starter modules and relies on Spring Boot's standard initialization SPI.What this approach gives:
@DependsOnDatabaseInitializationordering works automatically.exposed-core, non-Spring users, andDatabase.connect()semantics are untouched.DatabaseInitializerextension point is preserved.The cost: beans that need the schema during their own initialization must opt in with
@DependsOnDatabaseInitialization, whereas #2762 would make DDL run unconditionally at connect time. Exposing DDL config to non-Spring users (one of #2762's stated goals) can still be added later as a separate, focused change.Test plan
DatabaseInitializerEarlyInitTest(new, both modules) — a@DependsOnDatabaseInitialization-annotated bean queries the schema in itsafterPropertiesSet()and gets0Linstead of an exception. Exercises the full SPI loading + dependency ordering against a real H2 database.DatabaseInitializerTest(updated) — programmatic construction with an explicitDataSourceTransactionManagerstill works.ExposedAutoConfigurationTest— bothgenerate-ddl=trueand defaultfalsebranches.Locally:
./gradlew :exposed-spring-boot-starter:test_h2_v2 \ :exposed-spring-boot4-starter:test_h2_v2Type of Change
Please mark the relevant options with an "X":
Updates/remove existing public API methods:
Affected databases (Spring integration only — no dialect-specific changes; tests run against H2):
Checklist
Related Issues
https://youtrack.jetbrains.com/issue/EXPOSED-1004/SpringBoot-Starter-Database-Initializing-Before-Context-Refresh