From afcd3fcb1e530fdd3068cad68d3a2efb89337d6e Mon Sep 17 00:00:00 2001 From: Lennart Busekrus Date: Tue, 17 Mar 2026 16:56:34 +0100 Subject: [PATCH 1/4] fix(DatabaseInitializer): as InitializingBean ensures database initializing is done before springboot context refresh ref: EXPOSED-1004 --- .../api/exposed-spring-boot-starter.api | 10 ++---- .../v1/spring/boot/DatabaseInitializer.kt | 32 +++++++++++-------- .../v1/spring/boot/DatabaseInitializerTest.kt | 11 ++----- .../api/exposed-spring-boot4-starter.api | 10 ++---- .../v1/spring/boot4/DatabaseInitializer.kt | 28 ++++++++-------- .../spring/boot4/DatabaseInitializerTest.kt | 18 ++++------- 6 files changed, 46 insertions(+), 63 deletions(-) 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..fd96498011 100644 --- a/exposed-spring-boot-starter/api/exposed-spring-boot-starter.api +++ b/exposed-spring-boot-starter/api/exposed-spring-boot-starter.api @@ -1,12 +1,6 @@ -public class org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer : org/springframework/boot/ApplicationRunner, 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 class org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer : org/springframework/beans/factory/InitializingBean { public fun (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)V - public fun getOrder ()I - public fun run (Lorg/springframework/boot/ApplicationArguments;)V -} - -public final class org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer$Companion { + public fun afterPropertiesSet ()V } public final class org/jetbrains/exposed/v1/spring/boot/DatabaseInitializerKt { 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 2837ea6a86..70f7029b08 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 @@ -1,17 +1,17 @@ package org.jetbrains.exposed.v1.spring.boot +import org.jetbrains.exposed.v1.core.InternalApi import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.core.transactions.ThreadLocalTransactionsStack import org.jetbrains.exposed.v1.jdbc.SchemaUtils +import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager import org.slf4j.LoggerFactory -import org.springframework.boot.ApplicationArguments -import org.springframework.boot.ApplicationRunner +import org.springframework.beans.factory.InitializingBean 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 java.util.regex.Pattern /** @@ -23,24 +23,28 @@ import java.util.regex.Pattern * @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. */ -open class DatabaseInitializer(private val applicationContext: ApplicationContext, private val excludedPackages: List) : - ApplicationRunner, Ordered { - override fun getOrder(): Int = DATABASE_INITIALIZER_ORDER - - companion object { - const val DATABASE_INITIALIZER_ORDER = 0 - } +open class DatabaseInitializer( + private val applicationContext: ApplicationContext, + private val excludedPackages: List +) : InitializingBean { private val logger = LoggerFactory.getLogger(javaClass) - @Transactional - override fun run(args: ApplicationArguments?) { + override fun afterPropertiesSet() { + createTransaction() + val exposedTables = discoverExposedTables(applicationContext, excludedPackages) logger.info("Schema generation for tables '{}'", exposedTables.map { it.tableName }) logger.info("ddl {}", exposedTables.map { it.ddl }.joinToString()) SchemaUtils.create(tables = exposedTables.toTypedArray()) } + + @OptIn(InternalApi::class) + private fun createTransaction() { + val transaction = TransactionManager.manager.newTransaction(readOnly = false) + ThreadLocalTransactionsStack.pushTransaction(transaction) + } } /** @@ -52,6 +56,6 @@ fun discoverExposedTables(applicationContext: ApplicationContext, excludedPackag provider.addIncludeFilter(AssignableTypeFilter(Table::class.java)) excludedPackages.forEach { provider.addExcludeFilter(RegexPatternTypeFilter(Pattern.compile(it.replace(".", "\\.") + ".*"))) } val packages = AutoConfigurationPackages.get(applicationContext) - val components = packages.map { provider.findCandidateComponents(it) }.flatten() + val components = packages.flatMap { provider.findCandidateComponents(it) } return components.mapNotNull { Class.forName(it.beanClassName).kotlin.objectInstance as? Table } } 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..6e7e0b046a 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 @@ -3,7 +3,6 @@ package org.jetbrains.exposed.v1.spring.boot import org.jetbrains.exposed.v1.exceptions.ExposedSQLException import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.spring.boot.tables.TestTable import org.jetbrains.exposed.v1.spring.boot.tables.ignore.IgnoreTable import org.junit.jupiter.api.Assertions @@ -25,13 +24,9 @@ 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") - transaction { - DatabaseInitializer(applicationContext, listOf("org.jetbrains.exposed.v1.spring.boot.tables.ignore")).run( - null - ) - Assertions.assertEquals(0L, TestTable.selectAll().count()) - IgnoreTable.selectAll().count() - } + DatabaseInitializer(applicationContext, listOf("org.jetbrains.exposed.v1.spring.boot.tables.ignore")).afterPropertiesSet() + Assertions.assertEquals(0L, TestTable.selectAll().count()) + IgnoreTable.selectAll().count() } } 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..254cf2cc85 100644 --- a/exposed-spring-boot4-starter/api/exposed-spring-boot4-starter.api +++ b/exposed-spring-boot4-starter/api/exposed-spring-boot4-starter.api @@ -1,12 +1,6 @@ -public class org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer : org/springframework/boot/ApplicationRunner, 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 class org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer : org/springframework/beans/factory/InitializingBean { public fun (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)V - public fun getOrder ()I - public fun run (Lorg/springframework/boot/ApplicationArguments;)V -} - -public final class org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer$Companion { + public fun afterPropertiesSet ()V } public final class org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializerKt { 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 d5596d6427..346c3660c4 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 @@ -1,17 +1,17 @@ package org.jetbrains.exposed.v1.spring.boot4 +import org.jetbrains.exposed.v1.core.InternalApi import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.core.transactions.ThreadLocalTransactionsStack import org.jetbrains.exposed.v1.jdbc.SchemaUtils +import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager import org.slf4j.LoggerFactory -import org.springframework.boot.ApplicationArguments -import org.springframework.boot.ApplicationRunner +import org.springframework.beans.factory.InitializingBean 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 java.util.regex.Pattern /** @@ -26,23 +26,25 @@ import java.util.regex.Pattern open class DatabaseInitializer( private val applicationContext: ApplicationContext, private val excludedPackages: List -) : ApplicationRunner, Ordered { - override fun getOrder(): Int = DATABASE_INITIALIZER_ORDER - - companion object { - const val DATABASE_INITIALIZER_ORDER = 0 - } +) : InitializingBean { private val logger = LoggerFactory.getLogger(javaClass) - @Transactional - override fun run(args: ApplicationArguments) { + override fun afterPropertiesSet() { + createTransaction() + val exposedTables = discoverExposedTables(applicationContext, excludedPackages) logger.info("Schema generation for tables '{}'", exposedTables.map { it.tableName }) logger.info("ddl {}", exposedTables.map { it.ddl }.joinToString()) SchemaUtils.create(tables = exposedTables.toTypedArray()) } + + @OptIn(InternalApi::class) + private fun createTransaction() { + val transaction = TransactionManager.manager.newTransaction(readOnly = false) + ThreadLocalTransactionsStack.pushTransaction(transaction) + } } /** @@ -54,6 +56,6 @@ fun discoverExposedTables(applicationContext: ApplicationContext, excludedPackag provider.addIncludeFilter(AssignableTypeFilter(Table::class.java)) excludedPackages.forEach { provider.addExcludeFilter(RegexPatternTypeFilter(Pattern.compile(it.replace(".", "\\.") + ".*"))) } val packages = AutoConfigurationPackages.get(applicationContext) - val components = packages.map { provider.findCandidateComponents(it) }.flatten() + val components = packages.flatMap { provider.findCandidateComponents(it) } return components.mapNotNull { Class.forName(it.beanClassName).kotlin.objectInstance as? Table } } 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..b6eece0508 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 @@ -3,13 +3,11 @@ package org.jetbrains.exposed.v1.spring.boot4 import org.jetbrains.exposed.v1.exceptions.ExposedSQLException import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.spring.boot4.tables.TestTable 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 @@ -26,16 +24,12 @@ 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") - transaction { - val noArgs = DefaultApplicationArguments() - DatabaseInitializer( - applicationContext, - listOf("org.jetbrains.exposed.v1.spring.boot4.tables.ignore") - ) - .run(noArgs) - Assertions.assertEquals(0L, TestTable.selectAll().count()) - IgnoreTable.selectAll().count() - } + DatabaseInitializer( + applicationContext, + listOf("org.jetbrains.exposed.v1.spring.boot4.tables.ignore") + ).afterPropertiesSet() + Assertions.assertEquals(0L, TestTable.selectAll().count()) + IgnoreTable.selectAll().count() } } From f4646932751c986be0b20465aaa21f176234cbd6 Mon Sep 17 00:00:00 2001 From: lbsekr Date: Wed, 18 Mar 2026 14:25:32 +0100 Subject: [PATCH 2/4] feat: move ddl to database.kt ensures database initializing is done before springboot context refresh ref: EXPOSED-1004 --- exposed-core/api/exposed-core.api | 15 +++++ .../exposed/v1/core/DatabaseConfig.kt | 6 ++ .../org/jetbrains/exposed/v1/core/Ddl.kt | 5 ++ .../org/jetbrains/exposed/v1/jdbc/Database.kt | 23 ++++++- exposed-r2dbc/api/exposed-r2dbc.api | 1 + .../exposed/v1/r2dbc/R2dbcDatabase.kt | 22 ++++++- .../api/exposed-spring-boot-starter.api | 14 ++--- .../v1/spring/boot/DatabaseInitializer.kt | 61 ------------------- .../autoconfigure/ExposedAutoConfiguration.kt | 45 +++++++++----- .../v1/spring/boot/DatabaseInitializerTest.kt | 19 ++++-- .../ExposedAutoConfigurationTest.kt | 9 --- .../api/exposed-spring-boot4-starter.api | 14 ++--- .../v1/spring/boot4/DatabaseInitializer.kt | 61 ------------------- .../autoconfigure/ExposedAutoConfiguration.kt | 45 +++++++++----- .../spring/boot4/DatabaseInitializerTest.kt | 22 ++++--- .../ExposedAutoConfigurationTest.kt | 10 +-- 16 files changed, 169 insertions(+), 203 deletions(-) create mode 100644 exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/Ddl.kt delete mode 100644 exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer.kt delete mode 100644 exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer.kt diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index ce4f4b927e..d0a4bd45c6 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -611,6 +611,7 @@ public final class org/jetbrains/exposed/v1/core/DatabaseApi$Companion { public abstract interface class org/jetbrains/exposed/v1/core/DatabaseConfig { public static final field Companion Lorg/jetbrains/exposed/v1/core/DatabaseConfig$Companion; + public abstract fun getDdl ()Lorg/jetbrains/exposed/v1/core/Ddl; public abstract fun getDefaultFetchSize ()Ljava/lang/Integer; public abstract fun getDefaultIsolationLevel ()I public abstract fun getDefaultMaxAttempts ()I @@ -632,6 +633,7 @@ public abstract interface class org/jetbrains/exposed/v1/core/DatabaseConfig { public class org/jetbrains/exposed/v1/core/DatabaseConfig$Builder { public fun ()V + public final fun getDdl ()Lorg/jetbrains/exposed/v1/core/Ddl; public final fun getDefaultFetchSize ()Ljava/lang/Integer; public fun getDefaultIsolationLevel ()I public final fun getDefaultMaxAttempts ()I @@ -649,6 +651,7 @@ public class org/jetbrains/exposed/v1/core/DatabaseConfig$Builder { public final fun getSqlLogger ()Lorg/jetbrains/exposed/v1/core/SqlLogger; public final fun getUseNestedTransactions ()Z public final fun getWarnLongQueriesDuration ()Ljava/lang/Long; + public final fun setDdl (Lorg/jetbrains/exposed/v1/core/Ddl;)V public final fun setDefaultFetchSize (Ljava/lang/Integer;)V public fun setDefaultIsolationLevel (I)V public final fun setDefaultMaxAttempts (I)V @@ -675,6 +678,7 @@ public final class org/jetbrains/exposed/v1/core/DatabaseConfig$Companion { public class org/jetbrains/exposed/v1/core/DatabaseConfigImpl : org/jetbrains/exposed/v1/core/DatabaseConfig { public fun (Lorg/jetbrains/exposed/v1/core/DatabaseConfig$Builder;)V + public fun getDdl ()Lorg/jetbrains/exposed/v1/core/Ddl; public fun getDefaultFetchSize ()Ljava/lang/Integer; public fun getDefaultIsolationLevel ()I public fun getDefaultMaxAttempts ()I @@ -694,6 +698,17 @@ public class org/jetbrains/exposed/v1/core/DatabaseConfigImpl : org/jetbrains/ex public fun getWarnLongQueriesDuration ()Ljava/lang/Long; } +public final class org/jetbrains/exposed/v1/core/Ddl { + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lorg/jetbrains/exposed/v1/core/Ddl; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/v1/core/Ddl;Ljava/util/List;ILjava/lang/Object;)Lorg/jetbrains/exposed/v1/core/Ddl; + public fun equals (Ljava/lang/Object;)Z + public final fun getTables ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public abstract interface class org/jetbrains/exposed/v1/core/DdlAware { public abstract fun createStatement ()Ljava/util/List; public abstract fun dropStatement ()Ljava/util/List; diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/DatabaseConfig.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/DatabaseConfig.kt index 684e83f51a..cdce2157b2 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/DatabaseConfig.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/DatabaseConfig.kt @@ -28,6 +28,7 @@ interface DatabaseConfig { val logTooMuchResultSetsThreshold: Int val preserveKeywordCasing: Boolean val preserveIdentifierCasing: Boolean + val ddl: Ddl? /** * The [CoroutineDispatcher] to be used when determining the scope of Exposed transaction if @@ -183,6 +184,8 @@ interface DatabaseConfig { * Default dispatcher is [Dispatchers.IO]. */ var dispatcher: CoroutineDispatcher = IO + + var ddl: Ddl? = null } companion object { @@ -190,6 +193,7 @@ interface DatabaseConfig { val builder = Builder().apply(body) require(builder.defaultMaxAttempts > 0) { "defaultMaxAttempts must be set to perform at least 1 attempt." } + println(builder.ddl) @OptIn(InternalApi::class) return DatabaseConfigImpl(builder) } @@ -229,6 +233,8 @@ open class DatabaseConfigImpl(private val builder: DatabaseConfig.Builder) : Dat get() = builder.logTooMuchResultSetsThreshold override val dispatcher: CoroutineDispatcher get() = builder.dispatcher + override val ddl: Ddl? + get() = builder.ddl @OptIn(ExperimentalKeywordApi::class) override val preserveKeywordCasing: Boolean diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/Ddl.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/Ddl.kt new file mode 100644 index 0000000000..ebe7428080 --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/Ddl.kt @@ -0,0 +1,5 @@ +package org.jetbrains.exposed.v1.core + +data class Ddl( + val tables: List, +) diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/v1/jdbc/Database.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/v1/jdbc/Database.kt index d6056b7401..f84c792e9d 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/v1/jdbc/Database.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/v1/jdbc/Database.kt @@ -5,12 +5,14 @@ import org.jetbrains.exposed.v1.core.DatabaseConfig import org.jetbrains.exposed.v1.core.InternalApi import org.jetbrains.exposed.v1.core.Version import org.jetbrains.exposed.v1.core.statements.api.IdentifierManagerApi +import org.jetbrains.exposed.v1.core.transactions.withThreadLocalTransaction import org.jetbrains.exposed.v1.core.vendors.* import org.jetbrains.exposed.v1.jdbc.statements.api.ExposedConnection import org.jetbrains.exposed.v1.jdbc.statements.api.JdbcExposedDatabaseMetadata import org.jetbrains.exposed.v1.jdbc.transactions.JdbcTransactionManager import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager import org.jetbrains.exposed.v1.jdbc.vendors.* +import org.slf4j.LoggerFactory import java.sql.Connection import java.sql.DriverManager import java.util.* @@ -105,6 +107,8 @@ class Database private constructor( internal var dataSourceReadOnly: Boolean = false companion object { + private val logger = LoggerFactory.getLogger(this::class.java) + private val connectionInstanceImpl: DatabaseConnectionAutoRegistration by lazy { ServiceLoader.load(DatabaseConnectionAutoRegistration::class.java, Database::class.java.classLoader) .firstOrNull() @@ -174,10 +178,12 @@ class Database private constructor( setupConnection: (Connection) -> Unit = {}, manager: (Database) -> JdbcTransactionManager = { TransactionManager(it) } ): Database { - return Database(explicitVendor, config ?: DatabaseConfig.invoke()) { + val databaseConfig = config ?: DatabaseConfig.invoke() + return Database(explicitVendor, databaseConfig) { connectionAutoRegistration(getNewConnection().apply { setupConnection(this) }) }.apply { TransactionManager.registerManager(this, manager(this)) + generateSchema(this) } } @@ -296,6 +302,21 @@ class Database private constructor( fun getDialectName(url: String) = dialectMapping.entries.firstOrNull { (prefix, _) -> url.startsWith(prefix) }?.value + + @OptIn(InternalApi::class) + internal fun generateSchema(database: Database) { + if (database.config.ddl == null) { + logger.debug("Schema generation not configured") + return + } else { + withThreadLocalTransaction(TransactionManager.manager.newTransaction(readOnly = false)) { + val exposedTables = database.config.ddl!!.tables + logger.info("Schema generation for tables '{}'", exposedTables.map { it.tableName }) + logger.debug("ddl {}", exposedTables.map { it.ddl }.joinToString()) + SchemaUtils.create(tables = exposedTables.toTypedArray()) + } + } + } } } diff --git a/exposed-r2dbc/api/exposed-r2dbc.api b/exposed-r2dbc/api/exposed-r2dbc.api index 4440391a9e..83f7e310d6 100644 --- a/exposed-r2dbc/api/exposed-r2dbc.api +++ b/exposed-r2dbc/api/exposed-r2dbc.api @@ -271,6 +271,7 @@ public final class org/jetbrains/exposed/v1/r2dbc/R2dbcDatabaseConfig$Companion public final class org/jetbrains/exposed/v1/r2dbc/R2dbcDatabaseConfigImpl : org/jetbrains/exposed/v1/core/DatabaseConfig, org/jetbrains/exposed/v1/r2dbc/R2dbcDatabaseConfig { public fun (Lorg/jetbrains/exposed/v1/r2dbc/R2dbcDatabaseConfig$Builder;)V public fun getConnectionFactoryOptions ()Lio/r2dbc/spi/ConnectionFactoryOptions; + public fun getDdl ()Lorg/jetbrains/exposed/v1/core/Ddl; public fun getDefaultFetchSize ()Ljava/lang/Integer; public fun getDefaultIsolationLevel ()I public fun getDefaultMaxAttempts ()I diff --git a/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/R2dbcDatabase.kt b/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/R2dbcDatabase.kt index 7bd24e3e70..c682878445 100644 --- a/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/R2dbcDatabase.kt +++ b/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/R2dbcDatabase.kt @@ -17,6 +17,7 @@ import org.jetbrains.exposed.v1.r2dbc.statements.api.R2dbcLocalMetadataImpl import org.jetbrains.exposed.v1.r2dbc.transactions.R2dbcTransactionManager import org.jetbrains.exposed.v1.r2dbc.transactions.TransactionManager import org.jetbrains.exposed.v1.r2dbc.vendors.* +import org.slf4j.LoggerFactory import java.util.concurrent.ConcurrentHashMap /** @@ -121,6 +122,7 @@ class R2dbcDatabase private constructor( override val identifierManager: IdentifierManagerApi by lazy { connectionMetadata { identifierManager } } companion object { + private val logger = LoggerFactory.getLogger(this::class.java) private val dialectsMetadata = ConcurrentHashMap DatabaseDialectMetadata>() @@ -174,7 +176,10 @@ class R2dbcDatabase private constructor( connectionUrlMode = options.urlMode TransactionManager.registerManager(this, manager(this)) // ABOVE should be replaced with BELOW when ThreadLocalTransactionManager is fully deprecated - // TransactionManager.registerManager(this, manager(this)) + // TransactionManager.registerManager(this, manager(this))\ + runBlocking { + generateSchema(this@apply) + } } } @@ -293,6 +298,21 @@ class R2dbcDatabase private constructor( private fun getDriver(url: String) = driverMapping.entries.firstOrNull { (prefix, _) -> url.startsWith(prefix) }?.value ?: error("Database driver not found for $url") + + @OptIn(InternalApi::class) + private suspend fun generateSchema(database: R2dbcDatabase) { + if (database.config.ddl == null) { + logger.debug("Schema generation not configured") + return + } else { + withTransactionContext(TransactionManager.manager.newTransaction(readOnly = false), { + val exposedTables = database.config.ddl!!.tables + logger.info("Schema generation for tables '{}'", exposedTables.map { it.tableName }) + logger.debug("ddl {}", exposedTables.map { it.ddl }.joinToString()) + SchemaUtils.create(tables = exposedTables.toTypedArray()) + }) + } + } } } 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 fd96498011..ecaf17b91d 100644 --- a/exposed-spring-boot-starter/api/exposed-spring-boot-starter.api +++ b/exposed-spring-boot-starter/api/exposed-spring-boot-starter.api @@ -1,22 +1,16 @@ -public class org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer : org/springframework/beans/factory/InitializingBean { - public fun (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)V - public fun afterPropertiesSet ()V -} - -public final class org/jetbrains/exposed/v1/spring/boot/DatabaseInitializerKt { - public static final fun discoverExposedTables (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)Ljava/util/List; -} - public final class org/jetbrains/exposed/v1/spring/boot/ExposedAotContribution : org/springframework/beans/factory/aot/BeanFactoryInitializationAotProcessor { public fun ()V public fun processAheadOfTime (Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;)Lorg/springframework/beans/factory/aot/BeanFactoryInitializationAotContribution; } public class org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfiguration { + public static final field Companion Lorg/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfiguration$Companion; 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 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/ExposedAutoConfiguration$Companion { +} + 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 deleted file mode 100644 index 70f7029b08..0000000000 --- a/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer.kt +++ /dev/null @@ -1,61 +0,0 @@ -package org.jetbrains.exposed.v1.spring.boot - -import org.jetbrains.exposed.v1.core.InternalApi -import org.jetbrains.exposed.v1.core.Table -import org.jetbrains.exposed.v1.core.transactions.ThreadLocalTransactionsStack -import org.jetbrains.exposed.v1.jdbc.SchemaUtils -import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.InitializingBean -import org.springframework.boot.autoconfigure.AutoConfigurationPackages -import org.springframework.context.ApplicationContext -import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider -import org.springframework.core.type.filter.AssignableTypeFilter -import org.springframework.core.type.filter.RegexPatternTypeFilter -import java.util.regex.Pattern - -/** - * Base class responsible for the automatic creation of a database schema, using the results of [discoverExposedTables]. - * - * 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. - */ -open class DatabaseInitializer( - private val applicationContext: ApplicationContext, - private val excludedPackages: List -) : InitializingBean { - - private val logger = LoggerFactory.getLogger(javaClass) - - override fun afterPropertiesSet() { - createTransaction() - - val exposedTables = discoverExposedTables(applicationContext, excludedPackages) - logger.info("Schema generation for tables '{}'", exposedTables.map { it.tableName }) - - logger.info("ddl {}", exposedTables.map { it.ddl }.joinToString()) - SchemaUtils.create(tables = exposedTables.toTypedArray()) - } - - @OptIn(InternalApi::class) - private fun createTransaction() { - val transaction = TransactionManager.manager.newTransaction(readOnly = false) - ThreadLocalTransactionsStack.pushTransaction(transaction) - } -} - -/** - * Returns a list of identified tables that extend Exposed's base [Table] class, without searching any packages - * in [excludedPackages]. - */ -fun discoverExposedTables(applicationContext: ApplicationContext, excludedPackages: List): List
{ - val provider = ClassPathScanningCandidateComponentProvider(false) - provider.addIncludeFilter(AssignableTypeFilter(Table::class.java)) - excludedPackages.forEach { provider.addExcludeFilter(RegexPatternTypeFilter(Pattern.compile(it.replace(".", "\\.") + ".*"))) } - val packages = AutoConfigurationPackages.get(applicationContext) - val components = packages.flatMap { provider.findCandidateComponents(it) } - return components.mapNotNull { Class.forName(it.beanClassName).kotlin.objectInstance as? Table } -} 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..7019b90f9f 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 @@ -1,18 +1,23 @@ package org.jetbrains.exposed.v1.spring.boot.autoconfigure import org.jetbrains.exposed.v1.core.DatabaseConfig -import org.jetbrains.exposed.v1.spring.boot.DatabaseInitializer +import org.jetbrains.exposed.v1.core.Ddl +import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.spring.transaction.ExposedSpringTransactionAttributeSource import org.jetbrains.exposed.v1.spring.transaction.SpringTransactionManager import org.springframework.beans.factory.annotation.Value import org.springframework.boot.autoconfigure.AutoConfiguration +import org.springframework.boot.autoconfigure.AutoConfigurationPackages import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider import org.springframework.context.annotation.Primary +import org.springframework.core.type.filter.AssignableTypeFilter +import org.springframework.core.type.filter.RegexPatternTypeFilter import org.springframework.transaction.annotation.EnableTransactionManagement +import java.util.regex.Pattern import javax.sql.DataSource /** @@ -27,7 +32,6 @@ import javax.sql.DataSource * required values in a separate `@EnableTransactionManagement` on the main configuration class or in a configuration * file using `spring.aop.proxy-target-class`. * - * @property applicationContext The Spring ApplicationContext container responsible for managing beans. */ @AutoConfiguration(after = [DataSourceAutoConfiguration::class]) @EnableTransactionManagement @@ -36,6 +40,9 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC @Value($$"${spring.exposed.excluded-packages:}#{T(java.util.Collections).emptyList()}") private lateinit var excludedPackages: List + @Value($$"${spring.exposed.generate-ddl:false}") + private var generateDdl: Boolean = false + @Value($$"${spring.exposed.show-sql:false}") private var showSql: Boolean = false @@ -56,20 +63,13 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC @Bean @ConditionalOnMissingBean(DatabaseConfig::class) open fun databaseConfig(): DatabaseConfig { - return DatabaseConfig {} + return DatabaseConfig { + if (generateDdl) { + this.ddl = Ddl(discoverExposedTables(applicationContext, excludedPackages)) + } + } } - /** - * Returns a [DatabaseInitializer] that auto-creates the database schema, if enabled by the property - * `spring.exposed.generate-ddl` in the application.properties file. - * - * The property `spring.exposed.excluded-packages` can be used to ensure that tables in specified packages are - * not auto-created. - */ - @Bean - @ConditionalOnProperty("spring.exposed.generate-ddl", havingValue = "true", matchIfMissing = false) - open fun databaseInitializer() = DatabaseInitializer(applicationContext, excludedPackages) - /** * Returns an [ExposedSpringTransactionAttributeSource] instance. * @@ -83,4 +83,19 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC open fun exposedSpringTransactionAttributeSource(): ExposedSpringTransactionAttributeSource { return ExposedSpringTransactionAttributeSource() } + + companion object { + /** + * Returns a list of identified tables that extend Exposed's base [Table] class, without searching any packages + * in [excludedPackages]. + */ + internal fun discoverExposedTables(applicationContext: ApplicationContext, excludedPackages: List): List
{ + val provider = ClassPathScanningCandidateComponentProvider(false) + provider.addIncludeFilter(AssignableTypeFilter(Table::class.java)) + excludedPackages.forEach { provider.addExcludeFilter(RegexPatternTypeFilter(Pattern.compile(it.replace(".", "\\.") + ".*"))) } + val packages = AutoConfigurationPackages.get(applicationContext) + val components = packages.flatMap { provider.findCandidateComponents(it) } + return components.mapNotNull { Class.forName(it.beanClassName).kotlin.objectInstance as? Table } + } + } } 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 6e7e0b046a..bdcdde16bf 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 @@ -1,8 +1,12 @@ package org.jetbrains.exposed.v1.spring.boot +import org.jetbrains.exposed.v1.core.InternalApi +import org.jetbrains.exposed.v1.core.transactions.withThreadLocalTransaction import org.jetbrains.exposed.v1.exceptions.ExposedSQLException import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager +import org.jetbrains.exposed.v1.spring.boot.autoconfigure.ExposedAutoConfiguration import org.jetbrains.exposed.v1.spring.boot.tables.TestTable import org.jetbrains.exposed.v1.spring.boot.tables.ignore.IgnoreTable import org.junit.jupiter.api.Assertions @@ -13,27 +17,32 @@ import org.springframework.context.ApplicationContext @SpringBootTest( classes = [Application::class], - properties = ["spring.autoconfigure.exclude=org.jetbrains.exposed.v1.spring.boot.autoconfigure.ExposedAutoConfiguration"] + properties = [ + "spring.autoconfigure.exclude=org.jetbrains.exposed.v1.spring.boot.autoconfigure.ExposedAutoConfiguration", + "spring.exposed.excluded-packages=org.jetbrains.exposed.v1.spring.boot.tables.ignore" + ] ) open class DatabaseInitializerTest { @Autowired private lateinit var applicationContext: ApplicationContext + @OptIn(InternalApi::class) @Test 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") - DatabaseInitializer(applicationContext, listOf("org.jetbrains.exposed.v1.spring.boot.tables.ignore")).afterPropertiesSet() - Assertions.assertEquals(0L, TestTable.selectAll().count()) - IgnoreTable.selectAll().count() + withThreadLocalTransaction(TransactionManager.manager.newTransaction()) { + Assertions.assertEquals(0L, TestTable.selectAll().count()) + IgnoreTable.selectAll().count() + } } } @Test fun `ignore non object Table`() { Database.connect("jdbc:h2:mem:test-spring", user = "sa", driver = "org.h2.Driver") - val tables = discoverExposedTables(applicationContext, listOf()) + val tables = ExposedAutoConfiguration.discoverExposedTables(applicationContext, listOf()) Assertions.assertEquals(2, tables.size) // assertArrayEquals checks for order equality, which seems flaky? assert(TestTable in tables && IgnoreTable in tables) diff --git a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfigurationTest.kt b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfigurationTest.kt index 8679c48f6e..b668a3a1d1 100644 --- a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfigurationTest.kt +++ b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfigurationTest.kt @@ -4,7 +4,6 @@ import org.jetbrains.exposed.v1.core.DatabaseConfig import org.jetbrains.exposed.v1.core.ResultRow 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.jetbrains.exposed.v1.spring.transaction.ExposedSpringTransactionAttributeSource import org.jetbrains.exposed.v1.spring.transaction.SpringTransactionManager @@ -34,9 +33,6 @@ open class ExposedAutoConfigurationTest { @Autowired(required = false) private var springTransactionManager: SpringTransactionManager? = null - @Autowired(required = false) - private var databaseInitializer: DatabaseInitializer? = null - @Autowired private var databaseConfig: DatabaseConfig? = null @@ -48,11 +44,6 @@ open class ExposedAutoConfigurationTest { assertNotNull(springTransactionManager) } - @Test - fun `should not create schema`() { - assertNull(databaseInitializer) - } - @Test fun `database config can be overrode by custom one`() { val expectedConfig = CustomDatabaseConfigConfiguration.expectedConfig 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 254cf2cc85..eb0e49bb99 100644 --- a/exposed-spring-boot4-starter/api/exposed-spring-boot4-starter.api +++ b/exposed-spring-boot4-starter/api/exposed-spring-boot4-starter.api @@ -1,22 +1,16 @@ -public class org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer : org/springframework/beans/factory/InitializingBean { - public fun (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)V - public fun afterPropertiesSet ()V -} - -public final class org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializerKt { - public static final fun discoverExposedTables (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)Ljava/util/List; -} - public final class org/jetbrains/exposed/v1/spring/boot4/ExposedAotContribution : org/springframework/beans/factory/aot/BeanFactoryInitializationAotProcessor { public fun ()V public fun processAheadOfTime (Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;)Lorg/springframework/beans/factory/aot/BeanFactoryInitializationAotContribution; } public class org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfiguration { + public static final field Companion Lorg/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfiguration$Companion; 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 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/ExposedAutoConfiguration$Companion { +} + 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 deleted file mode 100644 index 346c3660c4..0000000000 --- a/exposed-spring-boot4-starter/src/main/kotlin/org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer.kt +++ /dev/null @@ -1,61 +0,0 @@ -package org.jetbrains.exposed.v1.spring.boot4 - -import org.jetbrains.exposed.v1.core.InternalApi -import org.jetbrains.exposed.v1.core.Table -import org.jetbrains.exposed.v1.core.transactions.ThreadLocalTransactionsStack -import org.jetbrains.exposed.v1.jdbc.SchemaUtils -import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.InitializingBean -import org.springframework.boot.autoconfigure.AutoConfigurationPackages -import org.springframework.context.ApplicationContext -import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider -import org.springframework.core.type.filter.AssignableTypeFilter -import org.springframework.core.type.filter.RegexPatternTypeFilter -import java.util.regex.Pattern - -/** - * Base class responsible for the automatic creation of a database schema, using the results of [discoverExposedTables]. - * - * 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. - */ -open class DatabaseInitializer( - private val applicationContext: ApplicationContext, - private val excludedPackages: List -) : InitializingBean { - - private val logger = LoggerFactory.getLogger(javaClass) - - override fun afterPropertiesSet() { - createTransaction() - - val exposedTables = discoverExposedTables(applicationContext, excludedPackages) - logger.info("Schema generation for tables '{}'", exposedTables.map { it.tableName }) - - logger.info("ddl {}", exposedTables.map { it.ddl }.joinToString()) - SchemaUtils.create(tables = exposedTables.toTypedArray()) - } - - @OptIn(InternalApi::class) - private fun createTransaction() { - val transaction = TransactionManager.manager.newTransaction(readOnly = false) - ThreadLocalTransactionsStack.pushTransaction(transaction) - } -} - -/** - * Returns a list of identified tables that extend Exposed's base [Table] class, without searching any packages - * in [excludedPackages]. - */ -fun discoverExposedTables(applicationContext: ApplicationContext, excludedPackages: List): List
{ - val provider = ClassPathScanningCandidateComponentProvider(false) - provider.addIncludeFilter(AssignableTypeFilter(Table::class.java)) - excludedPackages.forEach { provider.addExcludeFilter(RegexPatternTypeFilter(Pattern.compile(it.replace(".", "\\.") + ".*"))) } - val packages = AutoConfigurationPackages.get(applicationContext) - val components = packages.flatMap { provider.findCandidateComponents(it) } - return components.mapNotNull { Class.forName(it.beanClassName).kotlin.objectInstance as? Table } -} 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..1cf1fcaa4b 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 @@ -1,18 +1,23 @@ package org.jetbrains.exposed.v1.spring.boot4.autoconfigure import org.jetbrains.exposed.v1.core.DatabaseConfig -import org.jetbrains.exposed.v1.spring.boot4.DatabaseInitializer +import org.jetbrains.exposed.v1.core.Ddl +import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.spring7.transaction.ExposedSpringTransactionAttributeSource import org.jetbrains.exposed.v1.spring7.transaction.SpringTransactionManager import org.springframework.beans.factory.annotation.Value import org.springframework.boot.autoconfigure.AutoConfiguration +import org.springframework.boot.autoconfigure.AutoConfigurationPackages import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider import org.springframework.context.annotation.Primary +import org.springframework.core.type.filter.AssignableTypeFilter +import org.springframework.core.type.filter.RegexPatternTypeFilter import org.springframework.transaction.annotation.EnableTransactionManagement +import java.util.regex.Pattern import javax.sql.DataSource /** @@ -27,7 +32,6 @@ import javax.sql.DataSource * required values in a separate `@EnableTransactionManagement` on the main configuration class or in a configuration * file using `spring.aop.proxy-target-class`. * - * @property applicationContext The Spring ApplicationContext container responsible for managing beans. */ @AutoConfiguration(after = [DataSourceAutoConfiguration::class]) @EnableTransactionManagement @@ -36,6 +40,9 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC @Value($$"${spring.exposed.excluded-packages:}#{T(java.util.Collections).emptyList()}") private lateinit var excludedPackages: List + @Value($$"${spring.exposed.generate-ddl:false}") + private var generateDdl: Boolean = false + @Value($$"${spring.exposed.show-sql:false}") private var showSql: Boolean = false @@ -56,20 +63,13 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC @Bean @ConditionalOnMissingBean(DatabaseConfig::class) open fun databaseConfig(): DatabaseConfig { - return DatabaseConfig {} + return DatabaseConfig { + if (generateDdl) { + this.ddl = Ddl(discoverExposedTables(applicationContext, excludedPackages)) + } + } } - /** - * Returns a [DatabaseInitializer] that auto-creates the database schema, if enabled by the property - * `spring.exposed.generate-ddl` in the application.properties file. - * - * The property `spring.exposed.excluded-packages` can be used to ensure that tables in specified packages are - * not auto-created. - */ - @Bean - @ConditionalOnProperty("spring.exposed.generate-ddl", havingValue = "true", matchIfMissing = false) - open fun databaseInitializer() = DatabaseInitializer(applicationContext, excludedPackages) - /** * Returns an [ExposedSpringTransactionAttributeSource] instance. * @@ -83,4 +83,19 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC open fun exposedSpringTransactionAttributeSource(): ExposedSpringTransactionAttributeSource { return ExposedSpringTransactionAttributeSource() } + + companion object { + /** + * Returns a list of identified tables that extend Exposed's base [Table] class, without searching any packages + * in [excludedPackages]. + */ + internal fun discoverExposedTables(applicationContext: ApplicationContext, excludedPackages: List): List
{ + val provider = ClassPathScanningCandidateComponentProvider(false) + provider.addIncludeFilter(AssignableTypeFilter(Table::class.java)) + excludedPackages.forEach { provider.addExcludeFilter(RegexPatternTypeFilter(Pattern.compile(it.replace(".", "\\.") + ".*"))) } + val packages = AutoConfigurationPackages.get(applicationContext) + val components = packages.flatMap { provider.findCandidateComponents(it) } + return components.mapNotNull { Class.forName(it.beanClassName).kotlin.objectInstance as? Table } + } + } } 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 b6eece0508..c80cb10011 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 @@ -1,8 +1,12 @@ package org.jetbrains.exposed.v1.spring.boot4 +import org.jetbrains.exposed.v1.core.InternalApi +import org.jetbrains.exposed.v1.core.transactions.withThreadLocalTransaction import org.jetbrains.exposed.v1.exceptions.ExposedSQLException import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager +import org.jetbrains.exposed.v1.spring.boot4.autoconfigure.ExposedAutoConfiguration import org.jetbrains.exposed.v1.spring.boot4.tables.TestTable import org.jetbrains.exposed.v1.spring.boot4.tables.ignore.IgnoreTable import org.junit.jupiter.api.Assertions @@ -13,30 +17,32 @@ import org.springframework.context.ApplicationContext @SpringBootTest( classes = [Application::class], - properties = ["spring.autoconfigure.exclude=org.jetbrains.exposed.v1.spring.boot4.autoconfigure.ExposedAutoConfiguration"] + properties = [ + "spring.autoconfigure.exclude=org.jetbrains.exposed.v1.spring.boot4.autoconfigure.ExposedAutoConfiguration", + "spring.exposed.excluded-packages=org.jetbrains.exposed.v1.spring.boot4.tables.ignore" + ] ) open class DatabaseInitializerTest { @Autowired private lateinit var applicationContext: ApplicationContext + @OptIn(InternalApi::class) @Test 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") - DatabaseInitializer( - applicationContext, - listOf("org.jetbrains.exposed.v1.spring.boot4.tables.ignore") - ).afterPropertiesSet() - Assertions.assertEquals(0L, TestTable.selectAll().count()) - IgnoreTable.selectAll().count() + withThreadLocalTransaction(TransactionManager.manager.newTransaction()) { + Assertions.assertEquals(0L, TestTable.selectAll().count()) + IgnoreTable.selectAll().count() + } } } @Test fun `ignore non object Table`() { Database.connect("jdbc:h2:mem:test-spring", user = "sa", driver = "org.h2.Driver") - val tables = discoverExposedTables(applicationContext, listOf()) + val tables = ExposedAutoConfiguration.discoverExposedTables(applicationContext, listOf()) Assertions.assertEquals(2, tables.size) // assertArrayEquals checks for order equality, which seems flaky? assert(TestTable in tables && IgnoreTable in tables) diff --git a/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfigurationTest.kt b/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfigurationTest.kt index bcd995dd04..6402b8f457 100644 --- a/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfigurationTest.kt +++ b/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfigurationTest.kt @@ -4,7 +4,6 @@ import org.jetbrains.exposed.v1.core.DatabaseConfig import org.jetbrains.exposed.v1.core.ResultRow 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.jetbrains.exposed.v1.spring7.transaction.ExposedSpringTransactionAttributeSource import org.jetbrains.exposed.v1.spring7.transaction.SpringTransactionManager @@ -34,11 +33,8 @@ open class ExposedAutoConfigurationTest { @Autowired(required = false) private var springTransactionManager: SpringTransactionManager? = null - @Autowired(required = false) - private var databaseInitializer: DatabaseInitializer? = null - @Autowired - private var databaseConfig: DatabaseConfig? = null + private lateinit var databaseConfig: DatabaseConfig @Autowired private var transactionAttributeSource: TransactionAttributeSource? = null @@ -50,7 +46,7 @@ open class ExposedAutoConfigurationTest { @Test fun `should not create schema`() { - assertNull(databaseInitializer) + assertNull(databaseConfig.ddl) } @Test @@ -59,7 +55,7 @@ open class ExposedAutoConfigurationTest { assertSame(databaseConfig, expectedConfig) assertEquals( expectedConfig.maxEntitiesToStoreInCachePerEntity, - databaseConfig!!.maxEntitiesToStoreInCachePerEntity + databaseConfig.maxEntitiesToStoreInCachePerEntity ) } From b023521553c46de676fbcc8dbe5918b8d2e270e1 Mon Sep 17 00:00:00 2001 From: lbsekr Date: Wed, 18 Mar 2026 15:13:44 +0100 Subject: [PATCH 3/4] chore(JdbcTemplateTests): use dll table generation ref: EXPOSED-1004 --- .../v1/spring/boot/jdbc_template/JdbcTemplateTests.kt | 10 ---------- .../v1/spring/boot4/jdbc_template/JdbcTemplateTests.kt | 10 ---------- 2 files changed, 20 deletions(-) diff --git a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/jdbc_template/JdbcTemplateTests.kt b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/jdbc_template/JdbcTemplateTests.kt index 4f989beb9e..9c311f8dd8 100644 --- a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/jdbc_template/JdbcTemplateTests.kt +++ b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot/jdbc_template/JdbcTemplateTests.kt @@ -2,13 +2,10 @@ package org.jetbrains.exposed.v1.`jdbc-template` -import org.jetbrains.exposed.v1.jdbc.SchemaUtils -import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.junit.jupiter.api.* import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.test.context.SpringBootTest -import org.springframework.test.context.event.annotation.BeforeTestClass @SpringBootApplication open class JdbcTemplateApplication @@ -20,13 +17,6 @@ open class JdbcTemplateApplication @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class JdbcTemplateTests { - @BeforeTestClass - fun beforeTests() { - transaction { - SchemaUtils.create(AuthorTable, BookTable) - } - } - @Autowired lateinit var bookService: BookService diff --git a/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/jdbc_template/JdbcTemplateTests.kt b/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/jdbc_template/JdbcTemplateTests.kt index 4f989beb9e..9c311f8dd8 100644 --- a/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/jdbc_template/JdbcTemplateTests.kt +++ b/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/jdbc_template/JdbcTemplateTests.kt @@ -2,13 +2,10 @@ package org.jetbrains.exposed.v1.`jdbc-template` -import org.jetbrains.exposed.v1.jdbc.SchemaUtils -import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.junit.jupiter.api.* import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.test.context.SpringBootTest -import org.springframework.test.context.event.annotation.BeforeTestClass @SpringBootApplication open class JdbcTemplateApplication @@ -20,13 +17,6 @@ open class JdbcTemplateApplication @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class JdbcTemplateTests { - @BeforeTestClass - fun beforeTests() { - transaction { - SchemaUtils.create(AuthorTable, BookTable) - } - } - @Autowired lateinit var bookService: BookService From bb8b6f8c3ee3ed7d2569d0dce243168c7bf2072a Mon Sep 17 00:00:00 2001 From: lbsekr Date: Wed, 18 Mar 2026 16:32:22 +0100 Subject: [PATCH 4/4] fix(DatabaseInitializerTest): use transaction dsl ref: EXPOSED-1004 --- .../exposed/v1/spring/boot/DatabaseInitializerTest.kt | 5 ++--- .../exposed/v1/spring/boot4/DatabaseInitializerTest.kt | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) 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 bdcdde16bf..b8edd942de 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 @@ -1,11 +1,10 @@ package org.jetbrains.exposed.v1.spring.boot import org.jetbrains.exposed.v1.core.InternalApi -import org.jetbrains.exposed.v1.core.transactions.withThreadLocalTransaction import org.jetbrains.exposed.v1.exceptions.ExposedSQLException import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager +import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.spring.boot.autoconfigure.ExposedAutoConfiguration import org.jetbrains.exposed.v1.spring.boot.tables.TestTable import org.jetbrains.exposed.v1.spring.boot.tables.ignore.IgnoreTable @@ -32,7 +31,7 @@ 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") - withThreadLocalTransaction(TransactionManager.manager.newTransaction()) { + transaction { 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/DatabaseInitializerTest.kt b/exposed-spring-boot4-starter/src/test/kotlin/org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializerTest.kt index c80cb10011..5c5752cbf6 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 @@ -1,11 +1,10 @@ package org.jetbrains.exposed.v1.spring.boot4 import org.jetbrains.exposed.v1.core.InternalApi -import org.jetbrains.exposed.v1.core.transactions.withThreadLocalTransaction import org.jetbrains.exposed.v1.exceptions.ExposedSQLException import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager +import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.spring.boot4.autoconfigure.ExposedAutoConfiguration import org.jetbrains.exposed.v1.spring.boot4.tables.TestTable import org.jetbrains.exposed.v1.spring.boot4.tables.ignore.IgnoreTable @@ -32,7 +31,7 @@ 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") - withThreadLocalTransaction(TransactionManager.manager.newTransaction()) { + transaction { Assertions.assertEquals(0L, TestTable.selectAll().count()) IgnoreTable.selectAll().count() }