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!

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.

exposed-1004-initialization-order
  • Ensures safe application booting even when beans query the DB within @PostConstruct or afterPropertiesSet() (Note: Requires the @DependsOnDatabaseInitialization annotation or a recognized type).
  • Aligns with ecosystem conventions by following the exact same pattern as Flyway, Liquibase, and Spring Boot's built-in DataSourceInitializer.
  • Eliminates AOP proxy dependencies by combining the bean initialization phase with TransactionTemplate, ensuring a race-condition-free environment.
  • Applies the identical pattern across both Spring Boot 3 and 4 modules — Verified through empirical testing that the DatabaseInitializerDetector SPI mechanism works consistently in both versions (as Spring Boot 4 retains support for spring.factories loading).

The lifecycle choice (DDL in afterPropertiesSet()) follows JPA's pattern — LocalContainerEntityManagerFactoryBean.afterPropertiesSet() is where Hibernate's ddl-auto runs.

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.
The user gets a generic SQL error with no hint that DDL hasn't run yet.

This is unlike JPA, where LocalContainerEntityManagerFactoryBean triggers DDL 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 if anything
orders a user's bean after DatabaseInitializer, that bean is guaranteed
a ready schema.

(We use TransactionTemplate rather than @Transactional because the
AOP proxy that powers the annotation isn't active yet during
afterPropertiesSet().)

2. DatabaseInitializer is registered as a DatabaseInitializerDetector SPI.
Spring Boot's DatabaseInitializationDependencyConfigurer reads the SPI
and automatically 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.

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     // ← new annotation
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 putting 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 uses Spring Boot's standard
DatabaseInitializerDetector SPI — the same mechanism Flyway and Liquibase use. The trade-off:

  • @DependsOnDatabaseInitialization ordering works automatically.
  • exposed-core and non-Spring users untouched.
  • DatabaseInitializer extension 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 schema
    in its afterPropertiesSet() and gets 0L instead of an exception.
    Exercises the full Spring Boot 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 : Maybe?

Affected databases:

  • 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

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.

1 participant