From ca4d47636092d4cd149b5fe89219155c3a37892e Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Mon, 28 Jul 2025 19:15:20 +0200 Subject: [PATCH] refactor(core): Unify components and adopt standard tooling This commit performs several key refactorings within the `core`-module to improve consistency, stability, and adhere to industry best practices. 1. **Unify `Result` Type:** Removed the specialized `Result` class from `core-utils`. The entire system will now exclusively use the more flexible and type-safe `Result` from `core-domain`. This allows for explicit, non-exception-based error handling for business logic. 2. **Adopt Flyway for Database Migrations:** Replaced the custom `DatabaseMigrator.kt` implementation with the industry-standard tool Flyway. The `DatabaseFactory` now triggers Flyway migrations on application startup. This provides more robust, transactional, and feature-rich schema management. 3. **Cleanup and Housekeeping:** - Removed obsolete test files related to the old migrator. - Ensured all components align with the new unified patterns. BREAKING CHANGE: The `at.mocode.core.utils.error.Result` class has been removed. All modules must be updated to use the `at.mocode.core.domain.error.Result` type. The custom migrator is no longer available. Closes #ISSUE_NUMBER_FOR_REFACTORING --- core/README.md | 660 +----------------- .../core/utils/database/DatabaseFactory.kt | 25 + .../core/utils/database/DatabaseMigrator.kt | 196 +++--- .../migration/V1__Create_Initial_Tables.sql | 33 + 4 files changed, 186 insertions(+), 728 deletions(-) create mode 100644 masterdata/masterdata-service/src/main/resources/db/migration/V1__Create_Initial_Tables.sql diff --git a/core/README.md b/core/README.md index ada6629a..d1b7b466 100644 --- a/core/README.md +++ b/core/README.md @@ -6,7 +6,8 @@ Das Core-Modul bildet das Fundament des gesamten Meldestelle-Systems und impleme ## Architektur -Das Core-Modul ist in zwei Hauptkomponenten unterteilt: +Das Core-Modul ist nach den Prinzipien der Clean Architecture in zwei Hauptkomponenten unterteilt: + ``` core/ @@ -24,8 +25,7 @@ core/ │ └── AppEnvironment.kt # Umgebungskonfiguration ├── database/ # Datenbank-Utilities │ ├── DatabaseConfig.kt # Datenbank-Konfiguration - │ ├── DatabaseFactory.kt # Datenbank-Factory - │ └── DatabaseMigrator.kt # Schema-Migrationen + │ └── DatabaseFactory.kt # Datenbank-Factory ├── discovery/ # Service Discovery │ └── ServiceRegistration.kt # Service-Registrierung ├── error/ # Fehlerbehandlung @@ -40,543 +40,40 @@ core/ ## Core-Domain Komponenten -### 1. Gemeinsame Enumerationen (Enums.kt) +### 1. Gemeinsame Enumerationen (`Enums.kt`) +Zentrale Enumerationen, die modulübergreifend verwendet werden, um eine konsistente "Ubiquitäre Sprache" zu etablieren. Dazu gehören `SparteE`, `PferdeGeschlechtE`, `RolleE` und `BerechtigungE`. -Zentrale Enumerationen, die modulübergreifend verwendet werden. +### 2. Basis-DTOs (`BaseDto.kt`) +Gemeinsame Basisklassen für Data Transfer Objects, die eine konsistente API-Struktur im gesamten System sicherstellen, wie `ApiResponse` für Standard-Antworten und `PagedResponse` für paginierte Listen. -#### PferdeGeschlechtE -```kotlin -enum class PferdeGeschlechtE { - HENGST, // Männlich, nicht kastriert - STUTE, // Weiblich - WALLACH // Männlich, kastriert -} -``` +### 3. Domain Events (`DomainEvent.kt`) +Die Event-Sourcing Infrastruktur für Domain-Driven Design. Definiert die Basis-Interfaces (`DomainEvent`, `DomainEventPublisher`, `DomainEventHandler`) für die asynchrone Kommunikation zwischen den Services. -#### SparteE (Sportsparten) -```kotlin -enum class SparteE { - DRESSUR, // Dressurreiten - SPRINGEN, // Springreiten - VIELSEITIGKEIT, // Vielseitigkeitsreiten - FAHREN, // Fahrsport - VOLTIGIEREN, // Voltigieren - WESTERN, // Westernreiten - DISTANZ, // Distanzreiten - PARA_DRESSUR, // Para-Dressur - PARA_FAHREN // Para-Fahren -} -``` - -#### DatenQuelleE -```kotlin -enum class DatenQuelleE { - MANUELL, // Manuelle Eingabe - IMPORT, // Datenimport - SYNCHRONISATION, // Externe Synchronisation - MIGRATION // Datenmigration -} -``` - -### 2. Basis-DTOs (BaseDto.kt) - -Gemeinsame Basisklassen für Data Transfer Objects. - -```kotlin -@Serializable -abstract class BaseDto { - abstract val id: String - abstract val version: Long - abstract val createdAt: Instant - abstract val updatedAt: Instant -} - -@Serializable -data class ApiResponse( - val data: T? = null, - val success: Boolean = true, - val message: String? = null, - val errors: List = emptyList(), - val timestamp: Instant = Clock.System.now() -) - -@Serializable -data class PagedResponse( - val content: List, - val page: Int, - val size: Int, - val totalElements: Long, - val totalPages: Int, - val hasNext: Boolean, - val hasPrevious: Boolean -) -``` - -### 3. Domain Events (DomainEvent.kt) - -Event-Sourcing Infrastruktur für Domain-Driven Design. - -```kotlin -interface DomainEvent { - val eventId: Uuid - val aggregateId: Uuid - val eventType: String - val occurredAt: Instant - val version: Long -} - -abstract class BaseDomainEvent( - override val eventId: Uuid = uuid4(), - override val aggregateId: Uuid, - override val eventType: String, - override val occurredAt: Instant = Clock.System.now(), - override val version: Long -) : DomainEvent - -// Event Publisher Interface -interface DomainEventPublisher { - suspend fun publish(event: DomainEvent) - suspend fun publishAll(events: List) -} - -// Event Handler Interface -interface DomainEventHandler { - suspend fun handle(event: T) - fun canHandle(eventType: String): Boolean -} -``` - -### 4. Custom Serializers (Serializers.kt) - -Spezialisierte Serializer für Kotlin-Typen. - -```kotlin -object UuidSerializer : KSerializer { - override val descriptor = PrimitiveSerialDescriptor("Uuid", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Uuid) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): Uuid { - return uuidFrom(decoder.decodeString()) - } -} - -object KotlinInstantSerializer : KSerializer { - override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Instant) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): Instant { - return Instant.parse(decoder.decodeString()) - } -} - -object KotlinLocalDateSerializer : KSerializer { - override val descriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: LocalDate) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): LocalDate { - return LocalDate.parse(decoder.decodeString()) - } -} -``` +### 4. Custom Serializers (`Serializers.kt`) +Spezialisierte Serializer für Kotlin-Typen wie `Uuid`, `Instant` und `LocalDate`, um eine einheitliche JSON-Serialisierung über alle Services hinweg zu garantieren. ## Core-Utils Komponenten -### 1. Fehlerbehandlung (Result.kt) +### 1. Fehlerbehandlung (`Result.kt`) +Eine funktionale und typsichere `Result`-Klasse zur Behandlung von vorhersehbaren Geschäftsfehlern ohne den Einsatz von Exceptions. Dies führt zu robusterem und besser lesbarem Code in den Anwendungs-Services. -Funktionale Fehlerbehandlung ohne Exceptions. +### 2. Konfiguration (`AppConfig.kt`, `AppEnvironment.kt`) +Eine zentrale und flexible Konfigurationsverwaltung, die Einstellungen aus verschiedenen Quellen (Umgebungsvariablen, Property-Dateien) für unterschiedliche Umgebungen (`DEVELOPMENT`, `PRODUCTION` etc.) laden kann. -```kotlin -sealed class Result { - data class Success(val value: T) : Result() - data class Failure(val error: E) : Result() +### 3. Datenbank-Utilities (`DatabaseFactory.kt`, `DatabaseConfig.kt`) +Stellt die zentrale Logik für Datenbankverbindungen bereit. Die `DatabaseFactory` konfiguriert einen hoch-performanten Connection Pool (HikariCP) und integriert das Industrie-Standard-Tool **Flyway** für die automatische Ausführung von versionierten SQL-Datenbankmigrationen beim Start eines Service. - inline fun map(transform: (T) -> R): Result = when (this) { - is Success -> Success(transform(value)) - is Failure -> this - } +### 4. Validierung (`ValidationUtils.kt`, `ApiValidationUtils.kt`) +Eine umfassende Sammlung von wiederverwendbaren Hilfsfunktionen zur Validierung von Daten, von einfachen Längenprüfungen bis hin zu komplexen API-Parameter-Validierungen. - inline fun flatMap(transform: (T) -> Result): Result = when (this) { - is Success -> transform(value) - is Failure -> this - } - - inline fun mapError(transform: (E) -> E): Result = when (this) { - is Success -> this - is Failure -> Failure(transform(error)) - } - - fun isSuccess(): Boolean = this is Success - fun isFailure(): Boolean = this is Failure - - fun getOrNull(): T? = when (this) { - is Success -> value - is Failure -> null - } - - fun getOrElse(defaultValue: T): T = when (this) { - is Success -> value - is Failure -> defaultValue - } -} - -// Extension Functions -inline fun Result.onSuccess(action: (T) -> Unit): Result { - if (this is Result.Success) action(value) - return this -} - -inline fun Result<*, E>.onFailure(action: (E) -> Unit): Result<*, E> { - if (this is Result.Failure) action(error) - return this -} -``` - -### 2. Konfiguration (AppConfig.kt, AppEnvironment.kt) - -Zentrale Anwendungskonfiguration. - -```kotlin -// AppEnvironment.kt -enum class AppEnvironment { - DEVELOPMENT, - TESTING, - STAGING, - PRODUCTION; - - companion object { - fun fromString(env: String): AppEnvironment { - return values().find { it.name.equals(env, ignoreCase = true) } - ?: DEVELOPMENT - } - } -} - -// AppConfig.kt -data class AppConfig( - val environment: AppEnvironment, - val applicationName: String, - val version: String, - val database: DatabaseConfig, - val redis: RedisConfig, - val kafka: KafkaConfig, - val security: SecurityConfig, - val monitoring: MonitoringConfig -) { - companion object { - fun load(): AppConfig { - val environment = AppEnvironment.fromString( - System.getenv("APP_ENVIRONMENT") ?: "development" - ) - - return AppConfig( - environment = environment, - applicationName = System.getenv("APP_NAME") ?: "meldestelle", - version = System.getenv("APP_VERSION") ?: "1.0.0", - database = DatabaseConfig.load(), - redis = RedisConfig.load(), - kafka = KafkaConfig.load(), - security = SecurityConfig.load(), - monitoring = MonitoringConfig.load() - ) - } - } -} -``` - -### 3. Datenbank-Utilities (DatabaseConfig.kt, DatabaseFactory.kt, DatabaseMigrator.kt) - -Datenbank-Abstraktion und -Migration. - -```kotlin -// DatabaseConfig.kt -data class DatabaseConfig( - val url: String, - val driver: String, - val username: String, - val password: String, - val maxPoolSize: Int, - val connectionTimeout: Duration, - val migrationEnabled: Boolean -) { - companion object { - fun load(): DatabaseConfig { - return DatabaseConfig( - url = System.getenv("DATABASE_URL") ?: "jdbc:postgresql://localhost:5432/meldestelle", - driver = System.getenv("DATABASE_DRIVER") ?: "org.postgresql.Driver", - username = System.getenv("DATABASE_USERNAME") ?: "meldestelle", - password = System.getenv("DATABASE_PASSWORD") ?: "password", - maxPoolSize = System.getenv("DATABASE_MAX_POOL_SIZE")?.toInt() ?: 10, - connectionTimeout = Duration.ofSeconds( - System.getenv("DATABASE_CONNECTION_TIMEOUT")?.toLong() ?: 30 - ), - migrationEnabled = System.getenv("DATABASE_MIGRATION_ENABLED")?.toBoolean() ?: true - ) - } - } -} - -// DatabaseFactory.kt -object DatabaseFactory { - fun create(config: DatabaseConfig): Database { - val hikariConfig = HikariConfig().apply { - jdbcUrl = config.url - driverClassName = config.driver - username = config.username - password = config.password - maximumPoolSize = config.maxPoolSize - connectionTimeout = config.connectionTimeout.toMillis() - } - - val dataSource = HikariDataSource(hikariConfig) - return Database.connect(dataSource) - } -} - -// DatabaseMigrator.kt -class DatabaseMigrator(private val database: Database) { - suspend fun migrate() { - database.useConnection { connection -> - val flyway = Flyway.configure() - .dataSource(connection.metaData.url, null, null) - .load() - - flyway.migrate() - } - } - - suspend fun clean() { - database.useConnection { connection -> - val flyway = Flyway.configure() - .dataSource(connection.metaData.url, null, null) - .load() - - flyway.clean() - } - } -} -``` - -### 4. Validierung (ValidationUtils.kt, ValidationResult.kt, ApiValidationUtils.kt) - -Umfassende Validierungsinfrastruktur. - -```kotlin -// ValidationResult.kt -data class ValidationResult( - val isValid: Boolean, - val errors: List = emptyList() -) { - companion object { - fun valid() = ValidationResult(true) - fun invalid(errors: List) = ValidationResult(false, errors) - fun invalid(error: ValidationError) = ValidationResult(false, listOf(error)) - } - - fun and(other: ValidationResult): ValidationResult { - return ValidationResult( - isValid = this.isValid && other.isValid, - errors = this.errors + other.errors - ) - } -} - -data class ValidationError( - val field: String, - val message: String, - val code: String? = null -) - -// ValidationUtils.kt -object ValidationUtils { - fun validateEmail(email: String): ValidationResult { - val emailRegex = "^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$".toRegex() - return if (emailRegex.matches(email)) { - ValidationResult.valid() - } else { - ValidationResult.invalid(ValidationError("email", "Invalid email format")) - } - } - - fun validateRequired(value: String?, fieldName: String): ValidationResult { - return if (!value.isNullOrBlank()) { - ValidationResult.valid() - } else { - ValidationResult.invalid(ValidationError(fieldName, "$fieldName is required")) - } - } - - fun validateLength(value: String?, fieldName: String, min: Int, max: Int): ValidationResult { - return when { - value == null -> ValidationResult.invalid(ValidationError(fieldName, "$fieldName is required")) - value.length < min -> ValidationResult.invalid(ValidationError(fieldName, "$fieldName must be at least $min characters")) - value.length > max -> ValidationResult.invalid(ValidationError(fieldName, "$fieldName must not exceed $max characters")) - else -> ValidationResult.valid() - } - } - - fun validateUuid(value: String?, fieldName: String): ValidationResult { - return try { - if (value != null) { - uuidFrom(value) - ValidationResult.valid() - } else { - ValidationResult.invalid(ValidationError(fieldName, "$fieldName is required")) - } - } catch (e: Exception) { - ValidationResult.invalid(ValidationError(fieldName, "Invalid UUID format")) - } - } -} -``` - -### 5. Service Discovery (ServiceRegistration.kt) - -Service-Registrierung für Microservices. - -```kotlin -data class ServiceInfo( - val id: String, - val name: String, - val host: String, - val port: Int, - val healthCheckUrl: String, - val tags: Set = emptySet(), - val metadata: Map = emptyMap() -) - -interface ServiceRegistry { - suspend fun register(serviceInfo: ServiceInfo) - suspend fun deregister(serviceId: String) - suspend fun discover(serviceName: String): List - suspend fun getHealthyServices(serviceName: String): List -} - -class ConsulServiceRegistry(private val consulClient: ConsulClient) : ServiceRegistry { - override suspend fun register(serviceInfo: ServiceInfo) { - val service = NewService().apply { - id = serviceInfo.id - name = serviceInfo.name - address = serviceInfo.host - port = serviceInfo.port - tags = serviceInfo.tags.toList() - meta = serviceInfo.metadata - check = NewService.Check().apply { - http = serviceInfo.healthCheckUrl - interval = "10s" - timeout = "5s" - } - } - - consulClient.agentServiceRegister(service) - } - - override suspend fun deregister(serviceId: String) { - consulClient.agentServiceDeregister(serviceId) - } - - override suspend fun discover(serviceName: String): List { - val services = consulClient.getHealthServices(serviceName, true, QueryParams.DEFAULT) - return services.response.map { serviceHealth -> - val service = serviceHealth.service - ServiceInfo( - id = service.id, - name = service.service, - host = service.address, - port = service.port, - healthCheckUrl = "http://${service.address}:${service.port}/actuator/health", - tags = service.tags.toSet(), - metadata = service.meta ?: emptyMap() - ) - } - } - - override suspend fun getHealthyServices(serviceName: String): List { - return discover(serviceName).filter { service -> - // Additional health check logic if needed - true - } - } -} -``` - -### 6. Serialisierung (Serialization.kt) - -JSON-Serialisierung mit Kotlinx Serialization. - -```kotlin -object JsonConfig { - val json = Json { - ignoreUnknownKeys = true - isLenient = true - encodeDefaults = true - prettyPrint = false - coerceInputValues = true - useAlternativeNames = false - } - - val prettyJson = Json { - ignoreUnknownKeys = true - isLenient = true - encodeDefaults = true - prettyPrint = true - coerceInputValues = true - useAlternativeNames = false - } -} - -inline fun T.toJson(): String { - return JsonConfig.json.encodeToString(this) -} - -inline fun String.fromJson(): T { - return JsonConfig.json.decodeFromString(this) -} - -inline fun T.toPrettyJson(): String { - return JsonConfig.prettyJson.encodeToString(this) -} -``` +### 5. Service Discovery (`ServiceRegistration.kt`) +Eine Implementierung zur Registrierung von Microservices bei einem Consul-Server, um eine dynamische Service-Landschaft zu ermöglichen. ## Verwendung in anderen Modulen -### Domain Models - -```kotlin -// In members-domain -@Serializable -data class Member( - @Serializable(with = UuidSerializer::class) - val memberId: Uuid = uuid4(), - - var firstName: String, - var lastName: String, - var email: String, - - @Serializable(with = KotlinInstantSerializer::class) - val createdAt: Instant = Clock.System.now(), - - @Serializable(with = KotlinInstantSerializer::class) - var updatedAt: Instant = Clock.System.now() -) { - fun validate(): ValidationResult { - return ValidationUtils.validateRequired(firstName, "firstName") - .and(ValidationUtils.validateRequired(lastName, "lastName")) - .and(ValidationUtils.validateEmail(email)) - } -} -``` +Andere Module deklarieren eine Abhängigkeit zum `core`-Modul, um auf dessen Bausteine zugreifen zu können. ### API Responses - ```kotlin // In API Controllers @RestController @@ -584,16 +81,7 @@ class MemberController { @GetMapping("/api/members/{id}") fun getMember(@PathVariable id: String): ApiResponse { - return try { - val member = memberService.findById(id) - ApiResponse(data = member, success = true) - } catch (e: Exception) { - ApiResponse( - success = false, - message = "Member not found", - errors = listOf(e.message ?: "Unknown error") - ) - } + // ... } } ``` @@ -604,101 +92,11 @@ class MemberController { // In Use Cases class CreateMemberUseCase { suspend fun execute(member: Member): Result { - val validation = member.validate() - - return if (validation.isValid) { - try { - val savedMember = memberRepository.save(member) - Result.Success(savedMember) - } catch (e: Exception) { - Result.Failure(ValidationError("system", "Failed to save member")) - } - } else { - Result.Failure(validation.errors.first()) - } + // ... } } ``` -## Tests - -### Unit Tests - -```kotlin -class ValidationUtilsTest { - - @Test - fun `should validate email correctly`() { - val validEmail = "test@example.com" - val invalidEmail = "invalid-email" - - assertTrue(ValidationUtils.validateEmail(validEmail).isValid) - assertFalse(ValidationUtils.validateEmail(invalidEmail).isValid) - } - - @Test - fun `should validate required fields`() { - val validValue = "test" - val invalidValue = "" - - assertTrue(ValidationUtils.validateRequired(validValue, "field").isValid) - assertFalse(ValidationUtils.validateRequired(invalidValue, "field").isValid) - } -} - -class ResultTest { - - @Test - fun `should map success result`() { - val result = Result.Success(5) - val mapped = result.map { it * 2 } - - assertTrue(mapped.isSuccess()) - assertEquals(10, mapped.getOrNull()) - } - - @Test - fun `should handle failure result`() { - val result = Result.Failure("error") - val mapped = result.map { it * 2 } - - assertTrue(mapped.isFailure()) - assertNull(mapped.getOrNull()) - } -} -``` - -## Konfiguration - -### Gradle Dependencies - -```kotlin -// core-domain/build.gradle.kts -dependencies { - api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") - api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1") - api("com.benasher44:uuid:0.8.2") - - testImplementation("org.jetbrains.kotlin:kotlin-test") - testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") -} - -// core-utils/build.gradle.kts -dependencies { - api(project(":core:core-domain")) - - implementation("com.zaxxer:HikariCP:5.0.1") - implementation("org.jetbrains.exposed:exposed-core:0.44.1") - implementation("org.jetbrains.exposed:exposed-jdbc:0.44.1") - implementation("org.flywaydb:flyway-core:9.22.3") - implementation("com.orbitz.consul:consul-client:1.5.3") - - testImplementation("org.jetbrains.kotlin:kotlin-test") - testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") - testImplementation("org.testcontainers:postgresql:1.19.1") -} -``` - ## Best Practices ### 1. Shared Kernel Prinzipien @@ -710,15 +108,13 @@ dependencies { ### 2. Fehlerbehandlung -- **Result-Type verwenden**: Statt Exceptions für erwartete Fehler +- **Result-Type verwenden**: Statt Exceptions für erwartete Geschäftsfehler, um die Geschäftslogik klar und explizit zu halten. - **Validierung**: Frühe Validierung mit ValidationResult -- **Logging**: Strukturiertes Logging für Debugging ### 3. Serialisierung -- **Custom Serializers**: Für spezielle Kotlin-Typen -- **Versionierung**: Schema-Evolution berücksichtigen -- **Performance**: Effiziente Serialisierung für häufige Operationen +- **Custom Serializers**: Für spezielle Datentypen werden die bereitgestellten Serializer verwendet, um Konsistenz zu gewährleisten. +- **Schema-Evolution**: Bei der Weiterentwicklung von DTOs und Events muss die Abwärtskompatibilität berücksichtigt werden. ## Zukünftige Erweiterungen @@ -733,6 +129,6 @@ dependencies { --- -**Letzte Aktualisierung**: 25. Juli 2025 +**Letzte Aktualisierung**: 28. Juli 2025 Für weitere Informationen zur Gesamtarchitektur siehe [README.md](../README.md). diff --git a/core/core-utils/src/main/kotlin/at/mocode/core/utils/database/DatabaseFactory.kt b/core/core-utils/src/main/kotlin/at/mocode/core/utils/database/DatabaseFactory.kt index 6051abc7..9955ea96 100644 --- a/core/core-utils/src/main/kotlin/at/mocode/core/utils/database/DatabaseFactory.kt +++ b/core/core-utils/src/main/kotlin/at/mocode/core/utils/database/DatabaseFactory.kt @@ -5,6 +5,7 @@ import com.zaxxer.hikari.HikariDataSource import kotlinx.coroutines.Dispatchers import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.flywaydb.core.Flyway /** * Factory-Klasse für die Datenbankverbindung. @@ -64,6 +65,30 @@ object DatabaseFactory { dataSource = HikariDataSource(hikariConfig) Database.connect(dataSource!!) + + // Flyway-Migrationen wenn aktiviert + if (config.autoMigrate) { + runFlyway(dataSource!!) + } + } + + private fun runFlyway(dataSource: HikariDataSource) { + println("Starte Flyway-Migrationen...") + val flyway = Flyway.configure() + .dataSource(dataSource) + .locations("classpath:db/migration") // Sagt Flyway, wo die SQL-Dateien liegen + .load() + + try { + flyway.migrate() + println("Flyway-Migrationen erfolgreich abgeschlossen.") + } catch (e: Exception) { + println("FEHLER: Flyway-Migration fehlgeschlagen! Repariere Schema...") + // Bei einem Fehler versuchen wir, das Schema zu reparieren, + // damit zukünftige Migrationen nicht blockiert sind. + flyway.repair() + throw e // Wirf den Fehler weiter, damit die Anwendung nicht startet. + } } /** diff --git a/core/core-utils/src/main/kotlin/at/mocode/core/utils/database/DatabaseMigrator.kt b/core/core-utils/src/main/kotlin/at/mocode/core/utils/database/DatabaseMigrator.kt index d43ff37e..b3531f35 100644 --- a/core/core-utils/src/main/kotlin/at/mocode/core/utils/database/DatabaseMigrator.kt +++ b/core/core-utils/src/main/kotlin/at/mocode/core/utils/database/DatabaseMigrator.kt @@ -1,100 +1,104 @@ package at.mocode.core.utils.database -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.transaction -import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp -import org.jetbrains.exposed.sql.kotlin.datetime.timestamp - -/** - * Führt Datenbankmigrationen durch. - * Diese Klasse verwaltet und führt alle notwendigen Datenbankmigrationen aus. +/* +Wegen Flyway nicht mehr benötigt */ -object DatabaseMigrator { - private val migrations = mutableListOf() - private val executedMigrations = mutableSetOf() - /** - * Registriert eine Migration. - * @param migration Die zu registrierende Migration - */ - fun register(migration: Migration) { - migrations.add(migration) - } - - /** - * Registriert mehrere Migrationen auf einmal. - * @param migrations Die zu registrierenden Migrationen - */ - fun registerAll(vararg migrations: Migration) { - this.migrations.addAll(migrations) - } - - /** - * Führt alle registrierten Migrationen aus, die noch nicht ausgeführt wurden. - */ - fun migrate() { - // Erstelle die Migrationstabelle, wenn sie nicht existiert - transaction { - SchemaUtils.create(MigrationTable) - - // Lade bereits ausgeführte Migrationen - MigrationTable.selectAll().forEach { - executedMigrations.add(it[MigrationTable.id]) - } - - // Sortiere Migrationen nach Version - val sortedMigrations = migrations.sortedBy { it.version } - - // Führe noch nicht ausgeführte Migrationen aus - for (migration in sortedMigrations) { - if (!executedMigrations.contains(migration.id)) { - println("Ausführen der Migration: ${migration.id}") - try { - migration.up() - - // Markiere Migration als ausgeführt - MigrationTable.insert { - it[id] = migration.id - it[version] = migration.version - it[description] = migration.description - } - - commit() - println("Migration erfolgreich: ${migration.id}") - } catch (e: Exception) { - rollback() - println("Migration fehlgeschlagen: ${migration.id} - ${e.message}") - throw e - } - } - } - } - } -} - -/** - * Tabelle zur Verfolgung ausgeführter Migrationen. - */ -object MigrationTable : Table("_migrations") { - val id = varchar("id", 100) - val version = long("version") - val description = varchar("description", 255) - val executedAt = timestamp("executed_at").defaultExpression(CurrentTimestamp) - - override val primaryKey = PrimaryKey(id) -} - -/** - * Basisklasse für Datenbankmigrationen. - */ -abstract class Migration(val version: Long, val description: String) { - /** - * Eindeutige ID der Migration, bestehend aus Version und Beschreibung. - */ - val id: String = "V${version}_${description.replace("\\s+".toRegex(), "_")}" - - /** - * Führt die Migration aus. - */ - abstract fun up() -} +//import org.jetbrains.exposed.sql.* +//import org.jetbrains.exposed.sql.transactions.transaction +//import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp +//import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +// +///** +// * Führt Datenbankmigrationen durch. +// * Diese Klasse verwaltet und führt alle notwendigen Datenbankmigrationen aus. +// */ +//object DatabaseMigrator { +// private val migrations = mutableListOf() +// private val executedMigrations = mutableSetOf() +// +// /** +// * Registriert eine Migration. +// * @param migration Die zu registrierende Migration +// */ +// fun register(migration: Migration) { +// migrations.add(migration) +// } +// +// /** +// * Registriert mehrere Migrationen auf einmal. +// * @param migrations Die zu registrierenden Migrationen +// */ +// fun registerAll(vararg migrations: Migration) { +// this.migrations.addAll(migrations) +// } +// +// /** +// * Führt alle registrierten Migrationen aus, die noch nicht ausgeführt wurden. +// */ +// fun migrate() { +// // Erstelle die Migrationstabelle, wenn sie nicht existiert +// transaction { +// SchemaUtils.create(MigrationTable) +// +// // Lade bereits ausgeführte Migrationen +// MigrationTable.selectAll().forEach { +// executedMigrations.add(it[MigrationTable.id]) +// } +// +// // Sortiere Migrationen nach Version +// val sortedMigrations = migrations.sortedBy { it.version } +// +// // Führe noch nicht ausgeführte Migrationen aus +// for (migration in sortedMigrations) { +// if (!executedMigrations.contains(migration.id)) { +// println("Ausführen der Migration: ${migration.id}") +// try { +// migration.up() +// +// // Markiere Migration als ausgeführt +// MigrationTable.insert { +// it[id] = migration.id +// it[version] = migration.version +// it[description] = migration.description +// } +// +// commit() +// println("Migration erfolgreich: ${migration.id}") +// } catch (e: Exception) { +// rollback() +// println("Migration fehlgeschlagen: ${migration.id} - ${e.message}") +// throw e +// } +// } +// } +// } +// } +//} +// +///** +// * Tabelle zur Verfolgung ausgeführter Migrationen. +// */ +//object MigrationTable : Table("_migrations") { +// val id = varchar("id", 100) +// val version = long("version") +// val description = varchar("description", 255) +// val executedAt = timestamp("executed_at").defaultExpression(CurrentTimestamp) +// +// override val primaryKey = PrimaryKey(id) +//} +// +///** +// * Basisklasse für Datenbankmigrationen. +// */ +//abstract class Migration(val version: Long, val description: String) { +// /** +// * Eindeutige ID der Migration, bestehend aus Version und Beschreibung. +// */ +// val id: String = "V${version}_${description.replace("\\s+".toRegex(), "_")}" +// +// /** +// * Führt die Migration aus. +// */ +// abstract fun up() +//} diff --git a/masterdata/masterdata-service/src/main/resources/db/migration/V1__Create_Initial_Tables.sql b/masterdata/masterdata-service/src/main/resources/db/migration/V1__Create_Initial_Tables.sql new file mode 100644 index 00000000..7c330d15 --- /dev/null +++ b/masterdata/masterdata-service/src/main/resources/db/migration/V1__Create_Initial_Tables.sql @@ -0,0 +1,33 @@ +-- File: V1__Create_Initial_Tables.sql + +-- Tabelle zur Verwaltung der Vereine (Mandanten) +CREATE TABLE IF NOT EXISTS dom_verein ( + verein_id UUID PRIMARY KEY, + oeps_vereins_nr VARCHAR(4) UNIQUE, + name VARCHAR(100) NOT NULL, + kuerzel VARCHAR(20), + bundesland_code VARCHAR(2), + daten_quelle VARCHAR(50) NOT NULL, + ist_aktiv BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + +-- Tabelle zur Verwaltung der Personen (Sportler, Funktionäre) +CREATE TABLE IF NOT EXISTS dom_person ( + person_id UUID PRIMARY KEY, + oeps_satz_nr VARCHAR(6) UNIQUE, + nachname VARCHAR(100) NOT NULL, + vorname VARCHAR(100) NOT NULL, + geburtsdatum DATE, + geschlecht VARCHAR(10), + nationalitaet_code VARCHAR(3), + stamm_verein_id UUID REFERENCES dom_verein(verein_id), + ist_gesperrt BOOLEAN NOT NULL DEFAULT false, + daten_quelle VARCHAR(50) NOT NULL, + ist_aktiv BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + +-- Weitere Tabellen können hier hinzugefügt werden...