fix: EXPOSED-1004 SpringBoot-Starter Run DatabaseInitializer DDL during bean initialization#2820
Open
Jiwoo Kim (zbqmgldjfh) wants to merge 3 commits into
Open
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 | |||
Contributor
Author
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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!
Summary of the change:
Shifted DDL execution from after Spring Boot context refresh (ApplicationRunner) to the bean initialization phase (InitializingBean). It is now registered to Spring Boot's standard DatabaseInitializerDetector SPI, allowing automatic ordering for user beans via the @DependsOnDatabaseInitialization annotation.
@PostConstructorafterPropertiesSet()(Note: Requires the@DependsOnDatabaseInitializationannotation or a recognized type).DataSourceInitializer.TransactionTemplate, ensuring a race-condition-free environment.DatabaseInitializerDetectorSPI mechanism works consistently in both versions (as Spring Boot 4 retains support forspring.factoriesloading).The lifecycle choice (DDL in afterPropertiesSet()) follows JPA's pattern — LocalContainerEntityManagerFactoryBean.afterPropertiesSet() is where Hibernate's ddl-auto runs.
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.The user gets a generic SQL error with no hint that DDL hasn't run yet.
This is unlike JPA, where
LocalContainerEntityManagerFactoryBeantriggers DDL inside 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 if anythingorders a user's bean after
DatabaseInitializer, that bean is guaranteeda ready schema.
(We use
TransactionTemplaterather than@Transactionalbecause theAOP proxy that powers the annotation isn't active yet during
afterPropertiesSet().)2.
DatabaseInitializeris registered as aDatabaseInitializerDetectorSPI.Spring Boot's
DatabaseInitializationDependencyConfigurerreads the SPIand automatically adds a
dependsOnedge 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.
What does this mean for users?
For most users: nothing changes.
If you have
spring.exposed.generate-ddl=trueand your beans access thedatabase 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 putting DDL inside
Database.connect()via a newDatabaseConfig.ddlfield inexposed-core.This PR instead keeps the change scoped to the two starter modules and uses Spring Boot's standard
DatabaseInitializerDetectorSPI — the same mechanism Flyway and Liquibase use. The trade-off:@DependsOnDatabaseInitializationordering works automatically.exposed-coreand non-Spring users untouched.DatabaseInitializerextension point preserved.Database.connect()keeps its current semantics.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 schemain its
afterPropertiesSet()and gets0Linstead of an exception.Exercises the full Spring Boot SPI loading + dependency ordering
against a real H2 database.
DatabaseInitializerTest(updated) — programmatic constructionwith an explicit
DataSourceTransactionManagerstill works.ExposedAutoConfigurationTest— bothgenerate-ddl=trueanddefault
falsebranches.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:
Checklist
Related Issues
https://youtrack.jetbrains.com/issue/EXPOSED-1004/SpringBoot-Starter-Database-Initializing-Before-Context-Refresh