Skip to content

fix: EXPOSED-1004 SpringBoot-Starter Run DatabaseInitializer DDL during bean initialization - #2820

Open
Jiwoo Kim (zbqmgldjfh) wants to merge 3 commits into
JetBrains:mainfrom
zbqmgldjfh:zbqmgldjfh/exposed-1004
Open

fix: EXPOSED-1004 SpringBoot-Starter Run DatabaseInitializer DDL during bean initialization#2820
Jiwoo Kim (zbqmgldjfh) wants to merge 3 commits into
JetBrains:mainfrom
zbqmgldjfh:zbqmgldjfh/exposed-1004

Conversation

@zbqmgldjfh

@zbqmgldjfh Jiwoo Kim (zbqmgldjfh) commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

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 a DatabaseInitializerDetector via spring.factories so 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.

exposed-1004-initialization-order

The problem

The current DatabaseInitializer is an ApplicationRunner, which Spring Boot invokes after the context has finished refreshing. That's too late for beans that need the schema during their own initialization:

@Component
class Cache(private val tm: PlatformTransactionManager) : InitializingBean {
    override fun afterPropertiesSet() {
        TransactionTemplate(tm).execute {
            preload = MyTable.selectAll().count()   // table not found
        }
    }
}

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 LocalContainerEntityManagerFactoryBean runs ddl-auto inside its own afterPropertiesSet(), so dependent beans always find the schema ready.

The fix

Two changes, both standard Spring Boot patterns:

1. DatabaseInitializer is now an InitializingBean.
DDL runs inside afterPropertiesSet(), wrapped in a TransactionTemplate. This is the same lifecycle phase as @PostConstruct, so any bean ordered after DatabaseInitializer is guaranteed a ready schema. (TransactionTemplate is used instead of @Transactional because the AOP proxy behind the annotation isn't active yet during bean initialization.)

2. A DatabaseInitializerDetector is registered via spring.factories.
Spring Boot's DatabaseInitializationDependencyConfigurer picks it up and adds a dependsOn edge to every bean annotated with @DependsOnDatabaseInitialization — no manual @DependsOn wiring needed. This is the same SPI that Flyway and Liquibase use, so Exposed composes naturally with them. Verified that spring.factories loading 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=true and 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 @DependsOnDatabaseInitialization to the bean:

@Component
@DependsOnDatabaseInitialization     // ← opt in to early schema access
class Cache(...) : InitializingBean { ... }

Beans that depend on JdbcOperations / JdbcTemplate / JdbcClient get this for free — Spring Boot's built-in detector handles them.

Breaking change

The DatabaseInitializer constructor now requires a PlatformTransactionManager:

// before
DatabaseInitializer(applicationContext, excludedPackages)

// after
DatabaseInitializer(applicationContext, excludedPackages, transactionManager)

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 new DatabaseConfig.ddl field in exposed-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:

  • @DependsOnDatabaseInitialization ordering works automatically.
  • exposed-core, non-Spring users, and Database.connect() semantics are untouched.
  • The DatabaseInitializer extension 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 its afterPropertiesSet() and gets 0L instead of an exception. Exercises the full SPI loading + dependency ordering against a real H2 database.
  • DatabaseInitializerTest (updated) — programmatic construction with an explicit DataSourceTransactionManager still works.
  • ExposedAutoConfigurationTest — both generate-ddl=true and default false branches.
  • All existing tests pass unchanged.
  • Verified against Spring Boot 3.5.8 and 4.0.0 with H2.

Locally:

./gradlew :exposed-spring-boot-starter:test_h2_v2 \
          :exposed-spring-boot4-starter:test_h2_v2

Type of Change

Please mark the relevant options with an "X":

  • Bug fix
  • New feature
  • Documentation update

Updates/remove existing public API methods:

  • Is breaking change

Affected databases (Spring integration only — no dialect-specific changes; tests run against H2):

  • MariaDB
  • Mysql5
  • Mysql8
  • Oracle
  • Postgres
  • SqlServer
  • H2
  • SQLite

Checklist

  • Unit tests are in place
  • The build is green (including the Detekt check)
  • All public methods affected by my PR has up to date API docs
  • Documentation for my change is up to date

Related Issues

https://youtrack.jetbrains.com/issue/EXPOSED-1004/SpringBoot-Starter-Database-Initializing-Before-Context-Refresh

…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.
@zbqmgldjfh Jiwoo Kim (zbqmgldjfh) changed the title fix: EXPOSED-1004 Run DatabaseInitializer DDL during bean initialization fix: EXPOSED-1004 SpringBoot-Starter Run DatabaseInitializer DDL during bean initialization Jun 1, 2026
@@ -0,0 +1,2 @@
org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\
org.jetbrains.exposed.v1.spring.boot4.autoconfigure.ExposedDatabaseInitializerDetector

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

@zbqmgldjfh Jiwoo Kim (zbqmgldjfh) Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants