chore: migrate to Exposed 1.0.0 and refactor UUID handling

Updated persistence layer to align with Exposed 1.0.0. Refactored table definitions to use `javaUUID` for consistency with `java.util.UUID`. Adjusted transaction handling for compatibility with the latest API changes. Enhanced error handling in database utility functions and refined pagination logic. Added documentation and migration updates for best practices with Exposed 1.0.0.
This commit is contained in:
2026-02-02 12:34:24 +01:00
parent d4f20df654
commit 55e5302e13
12 changed files with 238 additions and 62 deletions
@@ -4,10 +4,11 @@ import at.mocode.core.domain.model.ErrorCodes
import at.mocode.core.domain.model.ErrorDto
import at.mocode.core.domain.model.PagedResponse
import at.mocode.core.utils.Result
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.statements.BatchInsertStatement
import org.jetbrains.exposed.v1.core.statements.StatementType
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.JdbcTransaction
import org.jetbrains.exposed.v1.jdbc.Query
@@ -28,9 +29,13 @@ inline fun <T> transactionResult(
crossinline block: JdbcTransaction.() -> T
): Result<T> {
return try {
val result = transaction(database) { block() }
// Wir nutzen hier explizit Exposed JDBC Transaktionen.
// Der Cast ist sicher, solange wir nur JDBC Databases verwenden (was wir tun).
val result = transaction(database) {
this.block()
}
Result.success(result)
} catch (e: SQLTimeoutException) {
} catch (_: SQLTimeoutException) {
Result.failure(
ErrorDto(
code = ErrorCodes.DATABASE_TIMEOUT,
@@ -40,26 +45,25 @@ inline fun <T> transactionResult(
} catch (e: SQLException) {
// Robustere Fehlerbehandlung über SQLSTATE (Postgres)
val mapped = when (e.sqlState) {
// unique_violation
"23505" -> ErrorCodes.DUPLICATE_ENTRY
// foreign_key_violation
"23503" -> ErrorCodes.FOREIGN_KEY_VIOLATION
// check_violation
"23514" -> ErrorCodes.CHECK_VIOLATION
"40001" -> ErrorCodes.DATABASE_ERROR // serialization_failure / deadlock
"08000", "08003", "08006" -> ErrorCodes.DATABASE_ERROR // connection errors
else -> ErrorCodes.DATABASE_ERROR
}
Result.failure(
ErrorDto(
code = mapped,
message = "Datenbank-Operation fehlgeschlagen"
message = "Datenbank-Operation fehlgeschlagen: ${e.message}"
)
)
} catch (e: Exception) {
Result.failure(
ErrorDto(
code = ErrorCodes.TRANSACTION_ERROR,
message = "Transaktion fehlgeschlagen"
message = "Transaktion fehlgeschlagen: ${e.message}"
)
)
}
@@ -87,14 +91,11 @@ fun <T> Query.toPagedResponse(
size: Int,
transform: (ResultRow) -> T
): PagedResponse<T> {
// Validate input parameters
require(page >= 0) { "Page number must be non-negative" }
require(size > 0) { "Page size must be positive" }
// Calculate the total count first (executes a COUNT query)
val totalCount = this.count()
// If there are no results, return an empty page
if (totalCount == 0L) {
return PagedResponse.create(
content = emptyList(),
@@ -107,23 +108,30 @@ fun <T> Query.toPagedResponse(
)
}
// Calculate total pages - use ceil division to ensure we round up
val totalPages = ((totalCount + size - 1) / size).toInt()
// Ensure the requested page exists (if page is beyond available pages, return the last page)
val adjustedPage = if (page >= totalPages) (totalPages - 1).coerceAtLeast(0) else page
if (page >= totalPages) {
return PagedResponse.create(
content = emptyList(),
page = page,
size = size,
totalElements = totalCount,
totalPages = totalPages,
hasNext = false,
hasPrevious = totalPages > 0
)
}
// Then apply pagination and transform results
val content = this.paginate(adjustedPage, size).map(transform)
val content = this.paginate(page, size).map(transform)
return PagedResponse.create(
content = content,
page = adjustedPage,
page = page,
size = size,
totalElements = totalCount,
totalPages = totalPages,
hasNext = adjustedPage < totalPages - 1,
hasPrevious = adjustedPage > 0
hasNext = page < totalPages - 1,
hasPrevious = page > 0
)
}
@@ -131,10 +139,10 @@ object DatabaseUtils {
fun tableExists(tableName: String, database: Database? = null): Boolean {
return transactionResult(database) {
// Postgres-spezifischer, robuster Ansatz über to_regclass
val valid = tableName.trim()
if (!valid.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) return@transactionResult false
exec("SELECT to_regclass('$valid')") { rs ->
this.exec("SELECT to_regclass('$valid')", explicitStatementType = StatementType.SELECT) { rs ->
if (rs.next()) rs.getString(1) else null
} != null
}.fold(
@@ -161,7 +169,6 @@ object DatabaseUtils {
database: Database? = null
): Result<Unit> {
return transactionResult(database) {
// Einfache Sanitization + Quoting der Identifier
fun quoteIdent(name: String): String {
require(name.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) { "Ungültiger Identifier: $name" }
return "\"$name\""
@@ -172,20 +179,27 @@ object DatabaseUtils {
val qIndex = quoteIdent(indexName)
val cols = columns.map { quoteIdent(it) }.joinToString(", ")
val sql = "CREATE $uniqueStr INDEX IF NOT EXISTS $qIndex ON $qTable ($cols)"
exec(sql)
this.exec(sql, explicitStatementType = StatementType.CREATE)
Unit
}
}
fun executeRawSql(sql: String, database: Database? = null): Result<Unit> = transactionResult(database) {
exec(sql)
this.exec(sql, explicitStatementType = StatementType.OTHER)
Unit
}
fun executeUpdate(sql: String, database: Database? = null): Result<Int> = transactionResult(database) {
// Nutzt Exposed PreparedStatementApi, kein AutoCloseable
val ps = this.connection.prepareStatement(sql, false)
ps.executeUpdate()
// Exposed 1.0.0: prepareStatement returns PreparedStatementApi which is NOT AutoCloseable
// and executeUpdate() might be missing on the interface or requires casting.
// We use the safe way via try-finally and closeIfPossible()
val stmt = this.connection.prepareStatement(sql, false)
try {
stmt.executeUpdate()
} finally {
stmt.closeIfPossible()
}
}
inline fun <T> batchInsert(
@@ -218,8 +232,7 @@ fun ResultRow.toMap(): Map<String, Any?> {
else -> result[expression.toString()] = this[expression]
}
} catch (e: Exception) {
// Ignore columns that can't be read and log the error if needed
// You could add logging here in a production environment
// Spalten ignorieren
}
}
return result
@@ -1,9 +1,10 @@
package at.mocode.events.infrastructure.persistence
import at.mocode.core.domain.model.SparteE
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.date
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
import org.jetbrains.exposed.v1.core.kotlin.datetime.date
import org.jetbrains.exposed.v1.core.kotlin.datetime.timestamp
import org.jetbrains.exposed.v1.core.javaUUID
/**
* Database table definition for events (Veranstaltung) in the event-management context.
@@ -24,7 +25,8 @@ object VeranstaltungTable : UUIDTable("veranstaltungen") {
// Location and Organization
val ort = varchar("ort", 255)
val veranstalterVereinId = uuid("veranstalter_verein_id")
// Migration to Exposed 1.0.0: Use javaUUID for java.util.UUID compatibility
val veranstalterVereinId = javaUUID("veranstalter_verein_id")
// Event Details
val sparten = text("sparten") // JSON array of SparteE values
@@ -2,9 +2,10 @@ package at.mocode.horses.infrastructure.persistence
import at.mocode.core.domain.model.PferdeGeschlechtE
import at.mocode.core.domain.model.DatenQuelleE
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.date
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
import org.jetbrains.exposed.v1.core.kotlin.datetime.date
import org.jetbrains.exposed.v1.core.kotlin.datetime.timestamp
import org.jetbrains.exposed.v1.core.javaUUID
/**
* Database table definition for horses in the horse-registry context.
@@ -21,8 +22,8 @@ object HorseTable : UUIDTable("horses") {
val farbe = varchar("farbe", 100).nullable()
// Ownership and Responsibility
val besitzerId = uuid("besitzer_id").nullable()
val verantwortlichePersonId = uuid("verantwortliche_person_id").nullable()
val besitzerId = javaUUID("besitzer_id").nullable()
val verantwortlichePersonId = javaUUID("verantwortliche_person_id").nullable()
// Breeding Information
val zuechterName = varchar("zuechter_name", 255).nullable()
@@ -1,8 +1,9 @@
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.javaUUID
/**
* Exposed-Tabellendefinition für die Altersklasse-Entität (Altersklassendefinitionen).
@@ -11,7 +12,7 @@ import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
* entsprechend der AltersklasseDefinition Domain-Entität.
*/
object AltersklasseTable : Table("altersklasse") {
val id = uuid("id").autoGenerate()
val id = javaUUID("id").autoGenerate()
val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex()
val bezeichnung = varchar("bezeichnung", 200)
val minAlter = integer("min_alter").nullable()
@@ -19,7 +20,7 @@ object AltersklasseTable : Table("altersklasse") {
val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable()
val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string
val geschlechtFilter = char("geschlecht_filter").nullable()
val oetoRegelReferenzId = uuid("oeto_regel_referenz_id").nullable()
val oetoRegelReferenzId = javaUUID("oeto_regel_referenz_id").nullable()
val istAktiv = bool("ist_aktiv").default(true)
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
@@ -1,8 +1,9 @@
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.javaUUID
/**
* Exposed-Tabellendefinition für die Bundesland-Entität (Bundesländer/Regionen).
@@ -11,8 +12,8 @@ import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
* Verwaltungseinheiten entsprechend der BundeslandDefinition Domain-Entität.
*/
object BundeslandTable : Table("bundesland") {
val id = uuid("id").autoGenerate()
val landId = uuid("land_id").references(LandTable.id)
val id = javaUUID("id").autoGenerate()
val landId = javaUUID("land_id").references(LandTable.id)
val oepsCode = varchar("oeps_code", 10).nullable()
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
val name = varchar("name", 100)
@@ -1,8 +1,9 @@
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.javaUUID
/**
* Exposed-Tabellendefinition für die Land-Entität (Länderstammdaten).
@@ -11,7 +12,7 @@ import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
* der LandDefinition Domain-Entität.
*/
object LandTable : Table("land") {
val id = uuid("id").autoGenerate()
val id = javaUUID("id").autoGenerate()
val isoAlpha2Code = varchar("iso_alpha2_code", 2).uniqueIndex()
val isoAlpha3Code = varchar("iso_alpha3_code", 3).uniqueIndex()
val isoNumerischerCode = varchar("iso_numerischer_code", 3).nullable()
@@ -1,8 +1,9 @@
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.javaUUID
/**
* Exposed-Tabellendefinition für die Platz-Entität (Turnierplätze/Wettkampfstätten).
@@ -11,8 +12,8 @@ import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
* entsprechend der Platz Domain-Entität.
*/
object PlatzTable : Table("platz") {
val id = uuid("id").autoGenerate()
val turnierId = uuid("turnier_id") // Foreign key to tournament (not enforced here as tournament might be in different module)
val id = javaUUID("id").autoGenerate()
val turnierId = javaUUID("turnier_id") // Foreign key to tournament (not enforced here as tournament might be in different module)
val name = varchar("name", 200)
val dimension = varchar("dimension", 50).nullable()
val boden = varchar("boden", 100).nullable()