diff --git a/documentation-website/Writerside/topics/Spring-Boot-integration.md b/documentation-website/Writerside/topics/Spring-Boot-integration.md index a2b547ae6f..9839c4eda6 100644 --- a/documentation-website/Writerside/topics/Spring-Boot-integration.md +++ b/documentation-website/Writerside/topics/Spring-Boot-integration.md @@ -144,7 +144,11 @@ spring.exposed.generate-ddl=true ``` When enabled, the starter detects all classes extending `org.jetbrains.exposed.v1.core.Table` and creates the schema -during application startup. +during the bean initialization phase, before any bean that depends on the schema is initialized. Beans that need to +access the database during their own initialization (for example, in `@PostConstruct` or +`InitializingBean.afterPropertiesSet()`) can declare this requirement with Spring Boot's +[`@DependsOnDatabaseInitialization`](https://docs.spring.io/spring-boot/api/kotlin/spring-boot-project/spring-boot/org.springframework.boot.sql.init.dependency/-depends-on-database-initialization/index.html) +annotation. Beans that use built-in detection points such as `JdbcOperations` are ordered automatically. ### Exclude packages @@ -256,18 +260,28 @@ When you build a native image, Spring Boot applies AOT processing. AOT restricts In particular, beans declared with `@ConditionalOnProperty` cannot change their behavior at runtime. As a result, setting `spring.exposed.generate-ddl=true` does not enable automatic schema creation in a native image. -Instead, create the database schema programmatically. For example: +Instead, create the database schema programmatically. Run it during the bean initialization phase via +`InitializingBean` so that downstream beans can rely on the schema being ready when their own initialization runs: ```kotlin @Component -@Transactional -class SchemaInitialize : ApplicationRunner { - override fun run(args: ApplicationArguments) { - SchemaUtils.create(MessageEntity) +class SchemaInitialize( + private val transactionManager: PlatformTransactionManager +) : InitializingBean { + override fun afterPropertiesSet() { + TransactionTemplate(transactionManager).execute { + SchemaUtils.create(MessageEntity) + } } } ``` +> Use programmatic transaction management with `TransactionTemplate` here rather than `@Transactional`. +> The AOP proxy that powers `@Transactional` is not yet active during `afterPropertiesSet()`, so the +> annotation would be silently ignored. +> +{style="note"} + ### Resolve `KotlinReflectionInternalError: Unresolved class` If the native image build or runtime fails with `KotlinReflectionInternalError: Unresolved class` the application likely diff --git a/exposed-spring-boot-starter/api/exposed-spring-boot-starter.api b/exposed-spring-boot-starter/api/exposed-spring-boot-starter.api index 41675d12e0..2abf7e2991 100644 --- a/exposed-spring-boot-starter/api/exposed-spring-boot-starter.api +++ b/exposed-spring-boot-starter/api/exposed-spring-boot-starter.api @@ -1,7 +1,8 @@ -public class org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer : org/springframework/boot/ApplicationRunner, org/springframework/core/Ordered { +public class org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer : org/springframework/beans/factory/InitializingBean, org/springframework/core/Ordered { public static final field Companion Lorg/jetbrains/exposed/v1/spring/boot/DatabaseInitializer$Companion; public static final field DATABASE_INITIALIZER_ORDER I - public fun (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)V + public fun (Lorg/springframework/context/ApplicationContext;Ljava/util/List;Lorg/springframework/transaction/PlatformTransactionManager;)V + public fun afterPropertiesSet ()V public fun getOrder ()I public fun run (Lorg/springframework/boot/ApplicationArguments;)V } @@ -21,8 +22,13 @@ public final class org/jetbrains/exposed/v1/spring/boot/ExposedAotContribution : public class org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfiguration { public fun (Lorg/springframework/context/ApplicationContext;)V public fun databaseConfig ()Lorg/jetbrains/exposed/v1/core/DatabaseConfig; - public fun databaseInitializer ()Lorg/jetbrains/exposed/v1/spring/boot/DatabaseInitializer; + public fun databaseInitializer (Lorg/jetbrains/exposed/v1/spring/transaction/SpringTransactionManager;)Lorg/jetbrains/exposed/v1/spring/boot/DatabaseInitializer; public fun exposedSpringTransactionAttributeSource ()Lorg/jetbrains/exposed/v1/spring/transaction/ExposedSpringTransactionAttributeSource; public fun springTransactionManager (Ljavax/sql/DataSource;Lorg/jetbrains/exposed/v1/core/DatabaseConfig;)Lorg/jetbrains/exposed/v1/spring/transaction/SpringTransactionManager; } +public final class org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedDatabaseInitializerDetector : org/springframework/boot/sql/init/dependency/DatabaseInitializerDetector { + public fun ()V + public fun detect (Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;)Ljava/util/Set; +} + diff --git a/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer.kt b/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer.kt index 4550656864..b985a66bd5 100644 --- a/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer.kt +++ b/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer.kt @@ -3,28 +3,29 @@ package org.jetbrains.exposed.v1.spring.boot import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.jdbc.SchemaUtils import org.slf4j.LoggerFactory +import org.springframework.beans.factory.InitializingBean import org.springframework.boot.ApplicationArguments -import org.springframework.boot.ApplicationRunner import org.springframework.boot.autoconfigure.AutoConfigurationPackages import org.springframework.context.ApplicationContext import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider import org.springframework.core.Ordered import org.springframework.core.type.filter.AssignableTypeFilter import org.springframework.core.type.filter.RegexPatternTypeFilter -import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.support.TransactionTemplate import java.util.regex.Pattern /** - * Base class responsible for the automatic creation of a database schema, using the results of [discoverExposedTables]. + * Creates the database schema using discovered [Table][org.jetbrains.exposed.v1.core.Table] objects + * during the bean initialization phase. * - * If more than just table creation is required, a derived class can be implemented to override the transactional - * function, [run], so that other schema operations can be performed when initialized. - * - * @property applicationContext The Spring ApplicationContext container responsible for managing beans. - * @property excludedPackages List of packages to exclude, so that their contained tables are not auto-created. + * A derived class can override [run] to perform additional schema operations. */ -open class DatabaseInitializer(private val applicationContext: ApplicationContext, private val excludedPackages: List) : - ApplicationRunner, Ordered { +open class DatabaseInitializer( + private val applicationContext: ApplicationContext, + private val excludedPackages: List, + private val transactionManager: PlatformTransactionManager +) : InitializingBean, Ordered { override fun getOrder(): Int = DATABASE_INITIALIZER_ORDER companion object { @@ -33,8 +34,16 @@ open class DatabaseInitializer(private val applicationContext: ApplicationContex private val logger = LoggerFactory.getLogger(javaClass) - @Transactional - override fun run(args: ApplicationArguments?) { + override fun afterPropertiesSet() { + TransactionTemplate(transactionManager).execute { + run(null) + } + } + + /** + * Discovers and creates database tables. Subclasses can override this to add custom schema operations. + */ + open fun run(args: ApplicationArguments?) { val exposedTables = discoverExposedTables(applicationContext, excludedPackages) logger.info("Schema generation for tables '{}'", exposedTables.map { it.tableName }) diff --git a/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfiguration.kt b/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfiguration.kt index 7398b12052..2bb70f8cbd 100644 --- a/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfiguration.kt +++ b/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfiguration.kt @@ -41,9 +41,6 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC /** * Returns a [SpringTransactionManager] instance using the specified [datasource] and [databaseConfig]. - * - * To enable logging of all transaction queries by the SpringTransactionManager instance, set the property - * `spring.exposed.show-sql` to `true` in the application.properties file. */ @Bean open fun springTransactionManager(datasource: DataSource, databaseConfig: DatabaseConfig): SpringTransactionManager { @@ -65,10 +62,15 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC * * The property `spring.exposed.excluded-packages` can be used to ensure that tables in specified packages are * not auto-created. + * + * DDL runs during the bean initialization phase via `InitializingBean.afterPropertiesSet`, before downstream + * beans annotated with `@DependsOnDatabaseInitialization` are initialized (ordering is enforced automatically + * via [ExposedDatabaseInitializerDetector]). */ @Bean @ConditionalOnProperty("spring.exposed.generate-ddl", havingValue = "true", matchIfMissing = false) - open fun databaseInitializer() = DatabaseInitializer(applicationContext, excludedPackages) + open fun databaseInitializer(springTransactionManager: SpringTransactionManager) = + DatabaseInitializer(applicationContext, excludedPackages, springTransactionManager) /** * Returns an [ExposedSpringTransactionAttributeSource] instance. diff --git a/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedDatabaseInitializerDetector.kt b/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedDatabaseInitializerDetector.kt new file mode 100644 index 0000000000..137789a826 --- /dev/null +++ b/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedDatabaseInitializerDetector.kt @@ -0,0 +1,20 @@ +package org.jetbrains.exposed.v1.spring.boot.autoconfigure + +import org.jetbrains.exposed.v1.spring.boot.DatabaseInitializer +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory +import org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector + +/** + * Registers Exposed's [DatabaseInitializer] as a database initializer bean for Spring Boot's automatic + * dependency ordering, so that beans annotated with + * [org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization] are initialized after the + * schema has been created by [DatabaseInitializer.afterPropertiesSet]. + */ +class ExposedDatabaseInitializerDetector : DatabaseInitializerDetector { + + override fun detect(beanFactory: ConfigurableListableBeanFactory): Set { + return beanFactory.getBeanNamesForType( + DatabaseInitializer::class.java, true, false + ).toSet() + } +} diff --git a/exposed-spring-boot-starter/src/main/resources/META-INF/spring.factories b/exposed-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..4039cbbd17 --- /dev/null +++ b/exposed-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\ +org.jetbrains.exposed.v1.spring.boot.autoconfigure.ExposedDatabaseInitializerDetector diff --git a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/DatabaseInitializerTest.kt b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/DatabaseInitializerTest.kt index 68b9dd37eb..0da99f9d8d 100644 --- a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/DatabaseInitializerTest.kt +++ b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/DatabaseInitializerTest.kt @@ -11,6 +11,8 @@ import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.context.ApplicationContext +import org.springframework.jdbc.datasource.DataSourceTransactionManager +import org.springframework.jdbc.datasource.SimpleDriverDataSource @SpringBootTest( classes = [Application::class], @@ -25,10 +27,14 @@ open class DatabaseInitializerTest { fun `should create schema for TestTable and not for IgnoreTable`() { Assertions.assertThrows(ExposedSQLException::class.java) { Database.connect("jdbc:h2:mem:test-spring", user = "sa", driver = "org.h2.Driver") + val dataSource = SimpleDriverDataSource(org.h2.Driver(), "jdbc:h2:mem:test-spring", "sa", "") + val txManager = DataSourceTransactionManager(dataSource) transaction { - DatabaseInitializer(applicationContext, listOf("org.jetbrains.exposed.v1.spring.boot.tables.ignore")).run( - null - ) + DatabaseInitializer( + applicationContext, + listOf("org.jetbrains.exposed.v1.spring.boot.tables.ignore"), + txManager + ).run(null) Assertions.assertEquals(0L, TestTable.selectAll().count()) IgnoreTable.selectAll().count() } diff --git a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/DatabaseInitializerEarlyInitTest.kt b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/DatabaseInitializerEarlyInitTest.kt new file mode 100644 index 0000000000..560fd864ae --- /dev/null +++ b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/DatabaseInitializerEarlyInitTest.kt @@ -0,0 +1,82 @@ +package org.jetbrains.exposed.v1.spring.boot.autoconfigure + +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.spring.boot.Application +import org.jetbrains.exposed.v1.spring.boot.DatabaseInitializer +import org.jetbrains.exposed.v1.spring.boot.tables.TestTable +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.InitializingBean +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.support.TransactionTemplate + +/** + * @author zbqmgldjfh@gmail.com + * Verifies that [DatabaseInitializer] is registered as a database initializer via Spring Boot's + * [DatabaseInitializerDetector][org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector] SPI, + * so that beans annotated with [DependsOnDatabaseInitialization] automatically wait for DDL to complete + * without requiring explicit `@DependsOn("databaseInitializer")`. + */ +@SpringBootTest( + classes = [Application::class, DatabaseInitializerEarlyInitTest.EarlyInitConfig::class], + properties = [ + "spring.datasource.url=jdbc:h2:mem:test-early-init;DB_CLOSE_DELAY=-1", + "spring.datasource.driver-class-name=org.h2.Driver", + "spring.exposed.generate-ddl=true" + ] +) +open class DatabaseInitializerEarlyInitTest { + + @TestConfiguration + open class EarlyInitConfig { + @Bean + @DependsOnDatabaseInitialization + @ConditionalOnProperty("spring.exposed.generate-ddl", havingValue = "true") + open fun schemaVerifier(transactionManager: PlatformTransactionManager): SchemaVerifierBean = + SchemaVerifierBean(transactionManager) + } + + /** + * A bean that queries the TestTable during its own initialization. + * Uses [DependsOnDatabaseInitialization] (Spring Boot standard) instead of + * `@DependsOn("databaseInitializer")` to verify SPI-based automatic ordering. + */ + class SchemaVerifierBean( + private val transactionManager: PlatformTransactionManager + ) : InitializingBean { + var tableRowCount: Long = -1 + + override fun afterPropertiesSet() { + TransactionTemplate(transactionManager).execute { + tableRowCount = TestTable.selectAll().count() + } + } + } + + @Autowired + private lateinit var schemaVerifier: SchemaVerifierBean + + @Test + fun `schema should be available during bean initialization via SPI`() { + assertEquals( + 0L, + schemaVerifier.tableRowCount, + "TestTable should be queryable during afterPropertiesSet with @DependsOnDatabaseInitialization" + ) + } + + @Test + fun `DatabaseInitializer should be an InitializingBean`() { + assertTrue( + InitializingBean::class.java.isAssignableFrom(DatabaseInitializer::class.java), + "DatabaseInitializer should implement InitializingBean" + ) + } +} diff --git a/exposed-spring-boot4-starter/api/exposed-spring-boot4-starter.api b/exposed-spring-boot4-starter/api/exposed-spring-boot4-starter.api index 54523f2490..7f91cef888 100644 --- a/exposed-spring-boot4-starter/api/exposed-spring-boot4-starter.api +++ b/exposed-spring-boot4-starter/api/exposed-spring-boot4-starter.api @@ -1,7 +1,8 @@ -public class org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer : org/springframework/boot/ApplicationRunner, org/springframework/core/Ordered { +public class org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer : org/springframework/beans/factory/InitializingBean, org/springframework/core/Ordered { public static final field Companion Lorg/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer$Companion; public static final field DATABASE_INITIALIZER_ORDER I - public fun (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)V + public fun (Lorg/springframework/context/ApplicationContext;Ljava/util/List;Lorg/springframework/transaction/PlatformTransactionManager;)V + public fun afterPropertiesSet ()V public fun getOrder ()I public fun run (Lorg/springframework/boot/ApplicationArguments;)V } @@ -21,8 +22,13 @@ public final class org/jetbrains/exposed/v1/spring/boot4/ExposedAotContribution public class org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfiguration { public fun (Lorg/springframework/context/ApplicationContext;)V public fun databaseConfig ()Lorg/jetbrains/exposed/v1/core/DatabaseConfig; - public fun databaseInitializer ()Lorg/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer; + public fun databaseInitializer (Lorg/jetbrains/exposed/v1/spring7/transaction/SpringTransactionManager;)Lorg/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer; public fun exposedSpringTransactionAttributeSource ()Lorg/jetbrains/exposed/v1/spring7/transaction/ExposedSpringTransactionAttributeSource; public fun springTransactionManager (Ljavax/sql/DataSource;Lorg/jetbrains/exposed/v1/core/DatabaseConfig;)Lorg/jetbrains/exposed/v1/spring7/transaction/SpringTransactionManager; } +public final class org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedDatabaseInitializerDetector : org/springframework/boot/sql/init/dependency/DatabaseInitializerDetector { + public fun ()V + public fun detect (Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;)Ljava/util/Set; +} + diff --git a/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer.kt b/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer.kt index 4ac599852d..fd1ff57d62 100644 --- a/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer.kt +++ b/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer.kt @@ -3,30 +3,29 @@ package org.jetbrains.exposed.v1.spring.boot4 import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.jdbc.SchemaUtils import org.slf4j.LoggerFactory +import org.springframework.beans.factory.InitializingBean import org.springframework.boot.ApplicationArguments -import org.springframework.boot.ApplicationRunner import org.springframework.boot.autoconfigure.AutoConfigurationPackages import org.springframework.context.ApplicationContext import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider import org.springframework.core.Ordered import org.springframework.core.type.filter.AssignableTypeFilter import org.springframework.core.type.filter.RegexPatternTypeFilter -import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.support.TransactionTemplate import java.util.regex.Pattern /** - * Base class responsible for the automatic creation of a database schema, using the results of [discoverExposedTables]. + * Creates the database schema using discovered [Table][org.jetbrains.exposed.v1.core.Table] objects + * during the bean initialization phase. * - * If more than just table creation is required, a derived class can be implemented to override the transactional - * function, [run], so that other schema operations can be performed when initialized. - * - * @property applicationContext The Spring ApplicationContext container responsible for managing beans. - * @property excludedPackages List of packages to exclude, so that their contained tables are not auto-created. + * A derived class can override [run] to perform additional schema operations. */ open class DatabaseInitializer( private val applicationContext: ApplicationContext, - private val excludedPackages: List -) : ApplicationRunner, Ordered { + private val excludedPackages: List, + private val transactionManager: PlatformTransactionManager +) : InitializingBean, Ordered { override fun getOrder(): Int = DATABASE_INITIALIZER_ORDER companion object { @@ -35,8 +34,16 @@ open class DatabaseInitializer( private val logger = LoggerFactory.getLogger(javaClass) - @Transactional - override fun run(args: ApplicationArguments) { + override fun afterPropertiesSet() { + TransactionTemplate(transactionManager).execute { + run(null) + } + } + + /** + * Discovers and creates database tables. Subclasses can override this to add custom schema operations. + */ + open fun run(args: ApplicationArguments?) { val exposedTables = discoverExposedTables(applicationContext, excludedPackages) logger.info("Schema generation for tables '{}'", exposedTables.map { it.tableName }) diff --git a/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfiguration.kt b/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfiguration.kt index 0b24db82d2..6ebd1f4393 100644 --- a/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfiguration.kt +++ b/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfiguration.kt @@ -65,10 +65,15 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC * * The property `spring.exposed.excluded-packages` can be used to ensure that tables in specified packages are * not auto-created. + * + * DDL runs during the bean initialization phase via `InitializingBean.afterPropertiesSet`, before downstream + * beans annotated with `@DependsOnDatabaseInitialization` are initialized (ordering is enforced automatically + * via [ExposedDatabaseInitializerDetector]). */ @Bean @ConditionalOnProperty("spring.exposed.generate-ddl", havingValue = "true", matchIfMissing = false) - open fun databaseInitializer() = DatabaseInitializer(applicationContext, excludedPackages) + open fun databaseInitializer(springTransactionManager: SpringTransactionManager) = + DatabaseInitializer(applicationContext, excludedPackages, springTransactionManager) /** * Returns an [ExposedSpringTransactionAttributeSource] instance. diff --git a/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedDatabaseInitializerDetector.kt b/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedDatabaseInitializerDetector.kt new file mode 100644 index 0000000000..6ae9906026 --- /dev/null +++ b/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedDatabaseInitializerDetector.kt @@ -0,0 +1,20 @@ +package org.jetbrains.exposed.v1.spring.boot4.autoconfigure + +import org.jetbrains.exposed.v1.spring.boot4.DatabaseInitializer +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory +import org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector + +/** + * Registers Exposed's [DatabaseInitializer] as a database initializer bean for Spring Boot's automatic + * dependency ordering, so that beans annotated with + * [org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization] are initialized after the + * schema has been created by [DatabaseInitializer.afterPropertiesSet]. + */ +class ExposedDatabaseInitializerDetector : DatabaseInitializerDetector { + + override fun detect(beanFactory: ConfigurableListableBeanFactory): Set { + return beanFactory.getBeanNamesForType( + DatabaseInitializer::class.java, true, false + ).toSet() + } +} diff --git a/exposed-spring-boot4-starter/src/main/resources/META-INF/spring.factories b/exposed-spring-boot4-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..86b6f18333 --- /dev/null +++ b/exposed-spring-boot4-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\ +org.jetbrains.exposed.v1.spring.boot4.autoconfigure.ExposedDatabaseInitializerDetector diff --git a/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializerTest.kt b/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializerTest.kt index 51353eeba6..37f5676b85 100644 --- a/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializerTest.kt +++ b/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializerTest.kt @@ -9,9 +9,10 @@ import org.jetbrains.exposed.v1.spring.boot4.tables.ignore.IgnoreTable import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.DefaultApplicationArguments import org.springframework.boot.test.context.SpringBootTest import org.springframework.context.ApplicationContext +import org.springframework.jdbc.datasource.DataSourceTransactionManager +import org.springframework.jdbc.datasource.SimpleDriverDataSource @SpringBootTest( classes = [Application::class], @@ -26,13 +27,14 @@ open class DatabaseInitializerTest { fun `should create schema for TestTable and not for IgnoreTable`() { Assertions.assertThrows(ExposedSQLException::class.java) { Database.connect("jdbc:h2:mem:test-spring", user = "sa", driver = "org.h2.Driver") + val dataSource = SimpleDriverDataSource(org.h2.Driver(), "jdbc:h2:mem:test-spring", "sa", "") + val txManager = DataSourceTransactionManager(dataSource) transaction { - val noArgs = DefaultApplicationArguments() DatabaseInitializer( applicationContext, - listOf("org.jetbrains.exposed.v1.spring.boot4.tables.ignore") - ) - .run(noArgs) + listOf("org.jetbrains.exposed.v1.spring.boot4.tables.ignore"), + txManager + ).run(null) Assertions.assertEquals(0L, TestTable.selectAll().count()) IgnoreTable.selectAll().count() } diff --git a/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/DatabaseInitializerEarlyInitTest.kt b/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/DatabaseInitializerEarlyInitTest.kt new file mode 100644 index 0000000000..8e4981f224 --- /dev/null +++ b/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/DatabaseInitializerEarlyInitTest.kt @@ -0,0 +1,81 @@ +package org.jetbrains.exposed.v1.spring.boot4.autoconfigure + +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.spring.boot4.Application +import org.jetbrains.exposed.v1.spring.boot4.DatabaseInitializer +import org.jetbrains.exposed.v1.spring.boot4.tables.TestTable +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.InitializingBean +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.support.TransactionTemplate + +/** + * Verifies that [DatabaseInitializer] is registered as a database initializer via Spring Boot's + * [DatabaseInitializerDetector][org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector] SPI, + * so that beans annotated with [DependsOnDatabaseInitialization] automatically wait for DDL to complete + * without requiring explicit `@DependsOn("databaseInitializer")`. + */ +@SpringBootTest( + classes = [Application::class, DatabaseInitializerEarlyInitTest.EarlyInitConfig::class], + properties = [ + "spring.datasource.url=jdbc:h2:mem:test-early-init;DB_CLOSE_DELAY=-1", + "spring.datasource.driver-class-name=org.h2.Driver", + "spring.exposed.generate-ddl=true" + ] +) +open class DatabaseInitializerEarlyInitTest { + + @TestConfiguration + open class EarlyInitConfig { + @Bean + @DependsOnDatabaseInitialization + @ConditionalOnProperty("spring.exposed.generate-ddl", havingValue = "true") + open fun schemaVerifier(transactionManager: PlatformTransactionManager): SchemaVerifierBean = + SchemaVerifierBean(transactionManager) + } + + /** + * A bean that queries the TestTable during its own initialization. + * Uses [DependsOnDatabaseInitialization] (Spring Boot standard) instead of + * `@DependsOn("databaseInitializer")` to verify SPI-based automatic ordering. + */ + class SchemaVerifierBean( + private val transactionManager: PlatformTransactionManager + ) : InitializingBean { + var tableRowCount: Long = -1 + + override fun afterPropertiesSet() { + TransactionTemplate(transactionManager).execute { + tableRowCount = TestTable.selectAll().count() + } + } + } + + @Autowired + private lateinit var schemaVerifier: SchemaVerifierBean + + @Test + fun `schema should be available during bean initialization via SPI`() { + assertEquals( + 0L, + schemaVerifier.tableRowCount, + "TestTable should be queryable during afterPropertiesSet with @DependsOnDatabaseInitialization" + ) + } + + @Test + fun `DatabaseInitializer should be an InitializingBean`() { + assertTrue( + InitializingBean::class.java.isAssignableFrom(DatabaseInitializer::class.java), + "DatabaseInitializer should implement InitializingBean" + ) + } +} diff --git a/samples/exposed-spring/src/main/kotlin/support/SchemaInitialize.kt b/samples/exposed-spring/src/main/kotlin/support/SchemaInitialize.kt index 7187c6246f..ca44b964f1 100644 --- a/samples/exposed-spring/src/main/kotlin/support/SchemaInitialize.kt +++ b/samples/exposed-spring/src/main/kotlin/support/SchemaInitialize.kt @@ -4,16 +4,19 @@ package org.jetbrains.exposed.samples.spring.support import org.jetbrains.exposed.samples.spring.domain.UserEntity import org.jetbrains.exposed.v1.jdbc.SchemaUtils -import org.springframework.boot.ApplicationArguments -import org.springframework.boot.ApplicationRunner +import org.springframework.beans.factory.InitializingBean import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.support.TransactionTemplate @Component -@Transactional -class SchemaInitialize : ApplicationRunner { +class SchemaInitialize( + private val transactionManager: PlatformTransactionManager +) : InitializingBean { - override fun run(args: ApplicationArguments) { - SchemaUtils.create(UserEntity) + override fun afterPropertiesSet() { + TransactionTemplate(transactionManager).execute { + SchemaUtils.create(UserEntity) + } } }