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:
+42
-29
@@ -4,10 +4,11 @@ import at.mocode.core.domain.model.ErrorCodes
|
|||||||
import at.mocode.core.domain.model.ErrorDto
|
import at.mocode.core.domain.model.ErrorDto
|
||||||
import at.mocode.core.domain.model.PagedResponse
|
import at.mocode.core.domain.model.PagedResponse
|
||||||
import at.mocode.core.utils.Result
|
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.ResultRow
|
||||||
import org.jetbrains.exposed.v1.core.Table
|
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.BatchInsertStatement
|
||||||
|
import org.jetbrains.exposed.v1.core.statements.StatementType
|
||||||
import org.jetbrains.exposed.v1.jdbc.Database
|
import org.jetbrains.exposed.v1.jdbc.Database
|
||||||
import org.jetbrains.exposed.v1.jdbc.JdbcTransaction
|
import org.jetbrains.exposed.v1.jdbc.JdbcTransaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.Query
|
import org.jetbrains.exposed.v1.jdbc.Query
|
||||||
@@ -28,9 +29,13 @@ inline fun <T> transactionResult(
|
|||||||
crossinline block: JdbcTransaction.() -> T
|
crossinline block: JdbcTransaction.() -> T
|
||||||
): Result<T> {
|
): Result<T> {
|
||||||
return try {
|
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)
|
Result.success(result)
|
||||||
} catch (e: SQLTimeoutException) {
|
} catch (_: SQLTimeoutException) {
|
||||||
Result.failure(
|
Result.failure(
|
||||||
ErrorDto(
|
ErrorDto(
|
||||||
code = ErrorCodes.DATABASE_TIMEOUT,
|
code = ErrorCodes.DATABASE_TIMEOUT,
|
||||||
@@ -40,26 +45,25 @@ inline fun <T> transactionResult(
|
|||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
// Robustere Fehlerbehandlung über SQLSTATE (Postgres)
|
// Robustere Fehlerbehandlung über SQLSTATE (Postgres)
|
||||||
val mapped = when (e.sqlState) {
|
val mapped = when (e.sqlState) {
|
||||||
// unique_violation
|
|
||||||
"23505" -> ErrorCodes.DUPLICATE_ENTRY
|
"23505" -> ErrorCodes.DUPLICATE_ENTRY
|
||||||
// foreign_key_violation
|
|
||||||
"23503" -> ErrorCodes.FOREIGN_KEY_VIOLATION
|
"23503" -> ErrorCodes.FOREIGN_KEY_VIOLATION
|
||||||
// check_violation
|
|
||||||
"23514" -> ErrorCodes.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
|
else -> ErrorCodes.DATABASE_ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
Result.failure(
|
Result.failure(
|
||||||
ErrorDto(
|
ErrorDto(
|
||||||
code = mapped,
|
code = mapped,
|
||||||
message = "Datenbank-Operation fehlgeschlagen"
|
message = "Datenbank-Operation fehlgeschlagen: ${e.message}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure(
|
Result.failure(
|
||||||
ErrorDto(
|
ErrorDto(
|
||||||
code = ErrorCodes.TRANSACTION_ERROR,
|
code = ErrorCodes.TRANSACTION_ERROR,
|
||||||
message = "Transaktion fehlgeschlagen"
|
message = "Transaktion fehlgeschlagen: ${e.message}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -87,14 +91,11 @@ fun <T> Query.toPagedResponse(
|
|||||||
size: Int,
|
size: Int,
|
||||||
transform: (ResultRow) -> T
|
transform: (ResultRow) -> T
|
||||||
): PagedResponse<T> {
|
): PagedResponse<T> {
|
||||||
// Validate input parameters
|
|
||||||
require(page >= 0) { "Page number must be non-negative" }
|
require(page >= 0) { "Page number must be non-negative" }
|
||||||
require(size > 0) { "Page size must be positive" }
|
require(size > 0) { "Page size must be positive" }
|
||||||
|
|
||||||
// Calculate the total count first (executes a COUNT query)
|
|
||||||
val totalCount = this.count()
|
val totalCount = this.count()
|
||||||
|
|
||||||
// If there are no results, return an empty page
|
|
||||||
if (totalCount == 0L) {
|
if (totalCount == 0L) {
|
||||||
return PagedResponse.create(
|
return PagedResponse.create(
|
||||||
content = emptyList(),
|
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()
|
val totalPages = ((totalCount + size - 1) / size).toInt()
|
||||||
|
|
||||||
// Ensure the requested page exists (if page is beyond available pages, return the last page)
|
if (page >= totalPages) {
|
||||||
val adjustedPage = if (page >= totalPages) (totalPages - 1).coerceAtLeast(0) else page
|
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(page, size).map(transform)
|
||||||
val content = this.paginate(adjustedPage, size).map(transform)
|
|
||||||
|
|
||||||
return PagedResponse.create(
|
return PagedResponse.create(
|
||||||
content = content,
|
content = content,
|
||||||
page = adjustedPage,
|
page = page,
|
||||||
size = size,
|
size = size,
|
||||||
totalElements = totalCount,
|
totalElements = totalCount,
|
||||||
totalPages = totalPages,
|
totalPages = totalPages,
|
||||||
hasNext = adjustedPage < totalPages - 1,
|
hasNext = page < totalPages - 1,
|
||||||
hasPrevious = adjustedPage > 0
|
hasPrevious = page > 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,10 +139,10 @@ object DatabaseUtils {
|
|||||||
|
|
||||||
fun tableExists(tableName: String, database: Database? = null): Boolean {
|
fun tableExists(tableName: String, database: Database? = null): Boolean {
|
||||||
return transactionResult(database) {
|
return transactionResult(database) {
|
||||||
// Postgres-spezifischer, robuster Ansatz über to_regclass
|
|
||||||
val valid = tableName.trim()
|
val valid = tableName.trim()
|
||||||
if (!valid.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) return@transactionResult false
|
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
|
if (rs.next()) rs.getString(1) else null
|
||||||
} != null
|
} != null
|
||||||
}.fold(
|
}.fold(
|
||||||
@@ -161,7 +169,6 @@ object DatabaseUtils {
|
|||||||
database: Database? = null
|
database: Database? = null
|
||||||
): Result<Unit> {
|
): Result<Unit> {
|
||||||
return transactionResult(database) {
|
return transactionResult(database) {
|
||||||
// Einfache Sanitization + Quoting der Identifier
|
|
||||||
fun quoteIdent(name: String): String {
|
fun quoteIdent(name: String): String {
|
||||||
require(name.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) { "Ungültiger Identifier: $name" }
|
require(name.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) { "Ungültiger Identifier: $name" }
|
||||||
return "\"$name\""
|
return "\"$name\""
|
||||||
@@ -172,20 +179,27 @@ object DatabaseUtils {
|
|||||||
val qIndex = quoteIdent(indexName)
|
val qIndex = quoteIdent(indexName)
|
||||||
val cols = columns.map { quoteIdent(it) }.joinToString(", ")
|
val cols = columns.map { quoteIdent(it) }.joinToString(", ")
|
||||||
val sql = "CREATE $uniqueStr INDEX IF NOT EXISTS $qIndex ON $qTable ($cols)"
|
val sql = "CREATE $uniqueStr INDEX IF NOT EXISTS $qIndex ON $qTable ($cols)"
|
||||||
exec(sql)
|
|
||||||
|
this.exec(sql, explicitStatementType = StatementType.CREATE)
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun executeRawSql(sql: String, database: Database? = null): Result<Unit> = transactionResult(database) {
|
fun executeRawSql(sql: String, database: Database? = null): Result<Unit> = transactionResult(database) {
|
||||||
exec(sql)
|
this.exec(sql, explicitStatementType = StatementType.OTHER)
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
fun executeUpdate(sql: String, database: Database? = null): Result<Int> = transactionResult(database) {
|
fun executeUpdate(sql: String, database: Database? = null): Result<Int> = transactionResult(database) {
|
||||||
// Nutzt Exposed PreparedStatementApi, kein AutoCloseable
|
// Exposed 1.0.0: prepareStatement returns PreparedStatementApi which is NOT AutoCloseable
|
||||||
val ps = this.connection.prepareStatement(sql, false)
|
// and executeUpdate() might be missing on the interface or requires casting.
|
||||||
ps.executeUpdate()
|
// 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(
|
inline fun <T> batchInsert(
|
||||||
@@ -218,8 +232,7 @@ fun ResultRow.toMap(): Map<String, Any?> {
|
|||||||
else -> result[expression.toString()] = this[expression]
|
else -> result[expression.toString()] = this[expression]
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Ignore columns that can't be read and log the error if needed
|
// Spalten ignorieren
|
||||||
// You could add logging here in a production environment
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
+6
-4
@@ -1,9 +1,10 @@
|
|||||||
package at.mocode.events.infrastructure.persistence
|
package at.mocode.events.infrastructure.persistence
|
||||||
|
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import org.jetbrains.exposed.dao.id.UUIDTable
|
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.date
|
import org.jetbrains.exposed.v1.core.kotlin.datetime.date
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
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.
|
* Database table definition for events (Veranstaltung) in the event-management context.
|
||||||
@@ -24,7 +25,8 @@ object VeranstaltungTable : UUIDTable("veranstaltungen") {
|
|||||||
|
|
||||||
// Location and Organization
|
// Location and Organization
|
||||||
val ort = varchar("ort", 255)
|
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
|
// Event Details
|
||||||
val sparten = text("sparten") // JSON array of SparteE values
|
val sparten = text("sparten") // JSON array of SparteE values
|
||||||
|
|||||||
+6
-5
@@ -2,9 +2,10 @@ package at.mocode.horses.infrastructure.persistence
|
|||||||
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
import at.mocode.core.domain.model.DatenQuelleE
|
||||||
import org.jetbrains.exposed.dao.id.UUIDTable
|
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.date
|
import org.jetbrains.exposed.v1.core.kotlin.datetime.date
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
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.
|
* Database table definition for horses in the horse-registry context.
|
||||||
@@ -21,8 +22,8 @@ object HorseTable : UUIDTable("horses") {
|
|||||||
val farbe = varchar("farbe", 100).nullable()
|
val farbe = varchar("farbe", 100).nullable()
|
||||||
|
|
||||||
// Ownership and Responsibility
|
// Ownership and Responsibility
|
||||||
val besitzerId = uuid("besitzer_id").nullable()
|
val besitzerId = javaUUID("besitzer_id").nullable()
|
||||||
val verantwortlichePersonId = uuid("verantwortliche_person_id").nullable()
|
val verantwortlichePersonId = javaUUID("verantwortliche_person_id").nullable()
|
||||||
|
|
||||||
// Breeding Information
|
// Breeding Information
|
||||||
val zuechterName = varchar("zuechter_name", 255).nullable()
|
val zuechterName = varchar("zuechter_name", 255).nullable()
|
||||||
|
|||||||
+6
-5
@@ -1,8 +1,9 @@
|
|||||||
package at.mocode.masterdata.infrastructure.persistence
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
import org.jetbrains.exposed.sql.Table
|
import org.jetbrains.exposed.v1.core.Table
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
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).
|
* 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.
|
* entsprechend der AltersklasseDefinition Domain-Entität.
|
||||||
*/
|
*/
|
||||||
object AltersklasseTable : Table("altersklasse") {
|
object AltersklasseTable : Table("altersklasse") {
|
||||||
val id = uuid("id").autoGenerate()
|
val id = javaUUID("id").autoGenerate()
|
||||||
val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex()
|
val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex()
|
||||||
val bezeichnung = varchar("bezeichnung", 200)
|
val bezeichnung = varchar("bezeichnung", 200)
|
||||||
val minAlter = integer("min_alter").nullable()
|
val minAlter = integer("min_alter").nullable()
|
||||||
@@ -19,7 +20,7 @@ object AltersklasseTable : Table("altersklasse") {
|
|||||||
val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable()
|
val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable()
|
||||||
val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string
|
val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string
|
||||||
val geschlechtFilter = char("geschlecht_filter").nullable()
|
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 istAktiv = bool("ist_aktiv").default(true)
|
||||||
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
|
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
|
||||||
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
|
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
|
||||||
|
|||||||
+6
-5
@@ -1,8 +1,9 @@
|
|||||||
package at.mocode.masterdata.infrastructure.persistence
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
import org.jetbrains.exposed.sql.Table
|
import org.jetbrains.exposed.v1.core.Table
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
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).
|
* 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.
|
* Verwaltungseinheiten entsprechend der BundeslandDefinition Domain-Entität.
|
||||||
*/
|
*/
|
||||||
object BundeslandTable : Table("bundesland") {
|
object BundeslandTable : Table("bundesland") {
|
||||||
val id = uuid("id").autoGenerate()
|
val id = javaUUID("id").autoGenerate()
|
||||||
val landId = uuid("land_id").references(LandTable.id)
|
val landId = javaUUID("land_id").references(LandTable.id)
|
||||||
val oepsCode = varchar("oeps_code", 10).nullable()
|
val oepsCode = varchar("oeps_code", 10).nullable()
|
||||||
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
|
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
|
||||||
val name = varchar("name", 100)
|
val name = varchar("name", 100)
|
||||||
|
|||||||
+5
-4
@@ -1,8 +1,9 @@
|
|||||||
package at.mocode.masterdata.infrastructure.persistence
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
import org.jetbrains.exposed.sql.Table
|
import org.jetbrains.exposed.v1.core.Table
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
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).
|
* 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.
|
* der LandDefinition Domain-Entität.
|
||||||
*/
|
*/
|
||||||
object LandTable : Table("land") {
|
object LandTable : Table("land") {
|
||||||
val id = uuid("id").autoGenerate()
|
val id = javaUUID("id").autoGenerate()
|
||||||
val isoAlpha2Code = varchar("iso_alpha2_code", 2).uniqueIndex()
|
val isoAlpha2Code = varchar("iso_alpha2_code", 2).uniqueIndex()
|
||||||
val isoAlpha3Code = varchar("iso_alpha3_code", 3).uniqueIndex()
|
val isoAlpha3Code = varchar("iso_alpha3_code", 3).uniqueIndex()
|
||||||
val isoNumerischerCode = varchar("iso_numerischer_code", 3).nullable()
|
val isoNumerischerCode = varchar("iso_numerischer_code", 3).nullable()
|
||||||
|
|||||||
+6
-5
@@ -1,8 +1,9 @@
|
|||||||
package at.mocode.masterdata.infrastructure.persistence
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
import org.jetbrains.exposed.sql.Table
|
import org.jetbrains.exposed.v1.core.Table
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
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).
|
* 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.
|
* entsprechend der Platz Domain-Entität.
|
||||||
*/
|
*/
|
||||||
object PlatzTable : Table("platz") {
|
object PlatzTable : Table("platz") {
|
||||||
val id = uuid("id").autoGenerate()
|
val id = javaUUID("id").autoGenerate()
|
||||||
val turnierId = uuid("turnier_id") // Foreign key to tournament (not enforced here as tournament might be in different module)
|
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 name = varchar("name", 200)
|
||||||
val dimension = varchar("dimension", 50).nullable()
|
val dimension = varchar("dimension", 50).nullable()
|
||||||
val boden = varchar("boden", 100).nullable()
|
val boden = varchar("boden", 100).nullable()
|
||||||
|
|||||||
+23
-3
@@ -96,19 +96,39 @@ Das Handling von JSONB-Spalten in SQLite wurde vereinheitlicht.
|
|||||||
5. JSON/SQLite:
|
5. JSON/SQLite:
|
||||||
- Verhalten von `jsonb()` mit `castToJsonFormat` prüfen und ggf. deaktivieren.
|
- Verhalten von `jsonb()` mit `castToJsonFormat` prüfen und ggf. deaktivieren.
|
||||||
|
|
||||||
## 3. Test-Matrix
|
## 3. Troubleshooting & Lessons Learned (Update 2026-02-02)
|
||||||
|
|
||||||
|
### 3.1 Low-Level JDBC Zugriff (`exec`, `executeUpdate`)
|
||||||
|
|
||||||
|
Bei der Migration von `DatabaseUtils.kt` traten Probleme mit der Auflösung von `exec` und `executeUpdate` auf.
|
||||||
|
|
||||||
|
* **Problem:** Die generische `Transaction` Klasse bietet in Exposed 1.0.0 keinen direkten Zugriff mehr auf `exec` mit `ResultSet`-Verarbeitung oder `executeUpdate`. Diese Methoden sind nun spezifischer in `JdbcTransaction` oder `PreparedStatementApi` verortet.
|
||||||
|
* **Lösung:**
|
||||||
|
* **Explizite `JdbcTransaction`:** Unsere Transaction-Wrapper (`transactionResult`) wurden angepasst, um `JdbcTransaction` als Receiver (`this`) zu erzwingen.
|
||||||
|
* **`exec` mit `StatementType`:** Aufrufe von `exec` müssen nun den `explicitStatementType` Parameter (z.B. `StatementType.SELECT`) nutzen, damit der Compiler die korrekte Überladung wählt.
|
||||||
|
* **`executeUpdate` Workaround:** Da `PreparedStatementApi` in Exposed 1.0.0 nicht `AutoCloseable` ist und `executeUpdate` teilweise schwer aufzulösen war, nutzen wir `try-finally` mit `closeIfPossible()` und greifen bei Bedarf auf die native JDBC Connection zu.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Beispiel für korrekten Low-Level Zugriff in Exposed 1.0.0
|
||||||
|
transactionResult(database) {
|
||||||
|
// 'this' ist JdbcTransaction
|
||||||
|
this.exec("SELECT ...", explicitStatementType = StatementType.SELECT) { rs -> ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Test-Matrix
|
||||||
|
|
||||||
- Unit: DSL-Typen (UUID, Zeittypen), Mappings, einfache Inserts/Selects.
|
- Unit: DSL-Typen (UUID, Zeittypen), Mappings, einfache Inserts/Selects.
|
||||||
- Integration: JDBC/Hikari Konfiguration, `batchInsert`, Upsert/Ignore-Pfade.
|
- Integration: JDBC/Hikari Konfiguration, `batchInsert`, Upsert/Ignore-Pfade.
|
||||||
- E2E (Docker): CRUD-Flows über den Service hinweg; Metriken/Health.
|
- E2E (Docker): CRUD-Flows über den Service hinweg; Metriken/Health.
|
||||||
|
|
||||||
## 4. Rollback-Plan
|
## 5. Rollback-Plan
|
||||||
|
|
||||||
- `git revert` des Version-Bumps in `libs.versions.toml` und ggf. betroffener Anpassungs-Commits.
|
- `git revert` des Version-Bumps in `libs.versions.toml` und ggf. betroffener Anpassungs-Commits.
|
||||||
- Rebuild; E2E-Smoketest durchführen.
|
- Rebuild; E2E-Smoketest durchführen.
|
||||||
- Dokumentenstatus auf `ARCHIVED` setzen oder Nachtrag mit „Rollback erfolgt“ ergänzen.
|
- Dokumentenstatus auf `ARCHIVED` setzen oder Nachtrag mit „Rollback erfolgt“ ergänzen.
|
||||||
|
|
||||||
## 5. Diagramm (Flow)
|
## 6. Diagramm (Flow)
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
type: Guide
|
||||||
|
status: DRAFT
|
||||||
|
owner: Backend Developer
|
||||||
|
date: 2026-02-02
|
||||||
|
---
|
||||||
|
|
||||||
|
# Database Best Practices & Exposed 1.0.0
|
||||||
|
|
||||||
|
Dieser Guide beschreibt den korrekten Umgang mit der Datenbank-Schicht in unseren Backend-Services, basierend auf JetBrains Exposed 1.0.0.
|
||||||
|
|
||||||
|
## 1. Architektur-Prinzipien
|
||||||
|
|
||||||
|
* **Trennung:** Datenbank-Zugriffe gehören ausschließlich in die `infrastructure/persistence` Schicht. Services nutzen Repositories (Interfaces), keine direkten Exposed-Aufrufe.
|
||||||
|
* **Transaktionen:** Jede geschäftliche Operation sollte in einer Transaktion laufen. Nutze dafür die Helper aus `DatabaseUtils.kt`.
|
||||||
|
|
||||||
|
## 2. Nutzung von `DatabaseUtils`
|
||||||
|
|
||||||
|
Wir haben zentrale Wrapper für Transaktionen, um Fehlerbehandlung und Logging zu vereinheitlichen.
|
||||||
|
|
||||||
|
### 2.1 Transaktionen starten
|
||||||
|
|
||||||
|
Nutze immer `transactionResult` (oder die Aliase `readTransaction` / `writeTransaction`), um Exposed-Code auszuführen.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun findUser(id: UUID): Result<User> = readTransaction {
|
||||||
|
// 'this' ist hier eine JdbcTransaction
|
||||||
|
UserTable.select { UserTable.id eq id }
|
||||||
|
.map { ... }
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtig:** Der Lambda-Receiver ist `JdbcTransaction`. Das ermöglicht Zugriff auf Low-Level JDBC Funktionen, falls nötig.
|
||||||
|
|
||||||
|
### 2.2 Low-Level SQL (`exec`, `executeUpdate`)
|
||||||
|
|
||||||
|
Vermeide rohes SQL, wo immer möglich. Wenn es sein muss (z.B. für Performance-Optimierungen oder spezielle Postgres-Features), beachte folgende Regeln für Exposed 1.0.0:
|
||||||
|
|
||||||
|
* **`exec`:** Nutze immer `explicitStatementType`.
|
||||||
|
```kotlin
|
||||||
|
this.exec("SELECT 1", explicitStatementType = StatementType.SELECT) { rs -> ... }
|
||||||
|
```
|
||||||
|
* **`executeUpdate`:** Nutze die Helper-Methode `DatabaseUtils.executeUpdate`, da sie sich um das korrekte Schließen von Statements kümmert (Exposed `PreparedStatementApi` ist nicht `AutoCloseable`).
|
||||||
|
|
||||||
|
## 3. Exposed 1.0.0 Besonderheiten
|
||||||
|
|
||||||
|
* **UUIDs:** Nutze `Table.javaUUID()` für `java.util.UUID` Spalten. `Table.uuid()` ist für `kotlin.uuid.Uuid` reserviert.
|
||||||
|
* **JSONB:** Bei SQLite wird JSON automatisch gewrappt. Prüfe `castToJsonFormat` Flag.
|
||||||
|
|
||||||
|
## 4. Fehlerbehandlung
|
||||||
|
|
||||||
|
`DatabaseUtils` fängt `SQLException` ab und mappt sie auf unsere Domain-Fehler (`ErrorDto`):
|
||||||
|
* Duplicate Key -> `ErrorCodes.DUPLICATE_ENTRY`
|
||||||
|
* Foreign Key -> `ErrorCodes.FOREIGN_KEY_VIOLATION`
|
||||||
|
* Timeout -> `ErrorCodes.DATABASE_TIMEOUT`
|
||||||
|
|
||||||
|
Wirf keine rohen Exceptions aus Repositories.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
type: Journal
|
||||||
|
status: COMPLETED
|
||||||
|
owner: Lead Architect
|
||||||
|
date: 2026-01-29
|
||||||
|
participants:
|
||||||
|
- Lead Architect
|
||||||
|
---
|
||||||
|
|
||||||
|
# Session Log: 29. Jänner 2026 - Roadmap Update & Phase 4 Kickoff
|
||||||
|
|
||||||
|
## Zielsetzung
|
||||||
|
Aktualisierung der MASTER ROADMAP nach erfolgreichem Abschluss der "Tracer Bullet" Phase und Vorbereitung auf den Start der fachlichen Implementierung (Phase 4).
|
||||||
|
|
||||||
|
## Durchgeführte Arbeiten
|
||||||
|
|
||||||
|
### 1. Status-Review
|
||||||
|
* Der "Tracer Bullet" (Ping-Service) ist erfolgreich durch den gesamten Stack implementiert.
|
||||||
|
* Kritische technische Hürden (SQLDelight Async, Docker Networking, ArchUnit) sind genommen.
|
||||||
|
* Das Projekt ist bereit für die Skalierung auf echte Fach-Domänen.
|
||||||
|
|
||||||
|
### 2. Roadmap Update
|
||||||
|
* **Phase 1-3:** Als "ABGESCHLOSSEN" markiert.
|
||||||
|
* **Phase 4 (Production Packaging & Domain Start):** Als "AKTUELL" definiert.
|
||||||
|
* **Neue Aufgaben:**
|
||||||
|
* **DevOps:** Dockerisierung des Frontends.
|
||||||
|
* **Backend:** Erstellung des `event-service`.
|
||||||
|
* **Frontend:** Erstellung des `events`-Features.
|
||||||
|
* **Architecture:** Sicherstellung der ArchUnit-Abdeckung für neue Module.
|
||||||
|
|
||||||
|
### 3. Architecture Review Vorbereitung
|
||||||
|
* Die bestehenden ArchUnit-Tests (`BackendArchitectureTest`, `FrontendArchitectureTest`) wurden analysiert.
|
||||||
|
* **Erkenntnis:**
|
||||||
|
* Backend-Tests erfordern manuelle Erweiterung der Package-Liste (`at.mocode.events..`).
|
||||||
|
* Frontend-Tests erfordern strikte Einhaltung der Package-Struktur (`at.mocode.<domain>.feature`).
|
||||||
|
* Diese Anforderungen wurden explizit in die Roadmap-Tasks aufgenommen.
|
||||||
|
|
||||||
|
## Ergebnis & Status
|
||||||
|
* Die Roadmap ist aktuell und spiegelt den Projektfortschritt wider.
|
||||||
|
* Die Aufgaben für die spezialisierten Agenten sind klar definiert.
|
||||||
|
* Der Fokus liegt nun auf der Erstellung der ersten echten Fachlichkeit ("Events").
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
type: Journal
|
||||||
|
status: COMPLETED
|
||||||
|
owner: Lead Architect
|
||||||
|
date: 2026-01-30
|
||||||
|
participants:
|
||||||
|
- Lead Architect
|
||||||
|
---
|
||||||
|
|
||||||
|
# Session Log: 30. Jänner 2026 - Refactoring Exposed & Ktor
|
||||||
|
|
||||||
|
## Zielsetzung
|
||||||
|
Durchführung der strategischen Migration auf Exposed 1.0.0 (Stable) und Ktor 3.4.0, basierend auf den technischen Analyseberichten.
|
||||||
|
|
||||||
|
## Durchgeführte Arbeiten
|
||||||
|
|
||||||
|
### 1. Versions-Update (`libs.versions.toml`)
|
||||||
|
* **Exposed:** Aktualisiert von `1.0.0-rc-4` auf `1.0.0`.
|
||||||
|
* **Ktor:** Aktualisiert von `3.3.3` auf `3.4.0`.
|
||||||
|
* **Neue Dependency:** `ktor-server-routing-openapi` hinzugefügt (für Backend OpenAPI Fix).
|
||||||
|
|
||||||
|
### 2. Exposed Migration (Backend)
|
||||||
|
* **Problem:** `DatabaseUtils.kt` enthielt veraltete/falsche Imports (`org.jetbrains.exposed.v1...`), die nicht mit Exposed 1.0.0 kompatibel sind.
|
||||||
|
* **Lösung:** Die Imports wurden auf die Standard-Exposed-Packages (`org.jetbrains.exposed.sql...`) korrigiert.
|
||||||
|
* **UUID-Thematik:** Die Tabellen-Definitionen (`VeranstaltungTable`, etc.) nutzen weiterhin `UUIDTable`. Es besteht das Risiko, dass Exposed 1.0.0 hier auf `kotlin.uuid.Uuid` gewechselt hat. Dies muss beim nächsten Build verifiziert werden. Falls Kompilierfehler auftreten, müssen die Tabellen auf `javaUUID` bzw. `JavaUUIDTable` (falls existent) migriert werden.
|
||||||
|
|
||||||
|
### 3. Ktor Migration (Frontend & Backend)
|
||||||
|
* **Frontend:** Das Build-Skript `frontend/core/network/build.gradle.kts` nutzt separate `js` und `wasmJs` Blöcke, daher war keine Umbenennung von `jsAndWasmShared` notwendig.
|
||||||
|
* **Backend:** Die Dependency `ktor-server-routing-openapi` wurde im Katalog bereitgestellt. Da die Backend-Module (Events, Horses, Masterdata) Ktor Server nutzen, aber keine explizite OpenAPI-Nutzung im Code gefunden wurde (wahrscheinlich SpringDoc), wurde hier kein Code geändert.
|
||||||
|
|
||||||
|
## Offene Punkte / Risiken
|
||||||
|
* **UUID Kompatibilität:** Die Verwendung von `UUIDTable` im Backend muss gegen Exposed 1.0.0 getestet werden. Es ist möglich, dass hier Breaking Changes zur Laufzeit oder Compile-Zeit auftreten.
|
||||||
|
* **Ktor JS Target:** Die separate Konfiguration von `js` und `wasmJs` ist funktional, aber das neue `web` Target wäre zukunftssicherer.
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
1. **Build & Test:** Ausführen des kompletten Builds (`./gradlew build`), um Kompilierfehler (insb. UUIDs) zu identifizieren.
|
||||||
|
2. **Runtime Test:** Starten des Backends und Prüfung der Datenbank-Interaktion.
|
||||||
+2
-2
@@ -37,9 +37,9 @@ class PingApiKoinClient(private val client: HttpClient) : PingApi {
|
|||||||
return client.get("/api/ping/secure").body()
|
return client.get("/api/ping/secure").body()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun syncPings(lastSyncTimestamp: Long): List<PingEvent> {
|
override suspend fun syncPings(since: Long): List<PingEvent> {
|
||||||
return client.get("/api/ping/sync") {
|
return client.get("/api/ping/sync") {
|
||||||
url.parameters.append("lastSyncTimestamp", lastSyncTimestamp.toString())
|
url.parameters.append("lastSyncTimestamp", since.toString())
|
||||||
}.body()
|
}.body()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user