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<T>` class from `core-utils`. The entire system will now exclusively use the more flexible and type-safe `Result<T, E>` 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
This commit is contained in:
+28
-632
@@ -6,7 +6,8 @@ Das Core-Modul bildet das Fundament des gesamten Meldestelle-Systems und impleme
|
|||||||
|
|
||||||
## Architektur
|
## Architektur
|
||||||
|
|
||||||
Das Core-Modul ist in zwei Hauptkomponenten unterteilt:
|
Das Core-Modul ist nach den Prinzipien der Clean Architecture in zwei Hauptkomponenten unterteilt:
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
core/
|
core/
|
||||||
@@ -24,8 +25,7 @@ core/
|
|||||||
│ └── AppEnvironment.kt # Umgebungskonfiguration
|
│ └── AppEnvironment.kt # Umgebungskonfiguration
|
||||||
├── database/ # Datenbank-Utilities
|
├── database/ # Datenbank-Utilities
|
||||||
│ ├── DatabaseConfig.kt # Datenbank-Konfiguration
|
│ ├── DatabaseConfig.kt # Datenbank-Konfiguration
|
||||||
│ ├── DatabaseFactory.kt # Datenbank-Factory
|
│ └── DatabaseFactory.kt # Datenbank-Factory
|
||||||
│ └── DatabaseMigrator.kt # Schema-Migrationen
|
|
||||||
├── discovery/ # Service Discovery
|
├── discovery/ # Service Discovery
|
||||||
│ └── ServiceRegistration.kt # Service-Registrierung
|
│ └── ServiceRegistration.kt # Service-Registrierung
|
||||||
├── error/ # Fehlerbehandlung
|
├── error/ # Fehlerbehandlung
|
||||||
@@ -40,543 +40,40 @@ core/
|
|||||||
|
|
||||||
## Core-Domain Komponenten
|
## 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<T>` für Standard-Antworten und `PagedResponse<T>` für paginierte Listen.
|
||||||
|
|
||||||
#### PferdeGeschlechtE
|
### 3. Domain Events (`DomainEvent.kt`)
|
||||||
```kotlin
|
Die Event-Sourcing Infrastruktur für Domain-Driven Design. Definiert die Basis-Interfaces (`DomainEvent`, `DomainEventPublisher`, `DomainEventHandler`) für die asynchrone Kommunikation zwischen den Services.
|
||||||
enum class PferdeGeschlechtE {
|
|
||||||
HENGST, // Männlich, nicht kastriert
|
|
||||||
STUTE, // Weiblich
|
|
||||||
WALLACH // Männlich, kastriert
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### SparteE (Sportsparten)
|
### 4. Custom Serializers (`Serializers.kt`)
|
||||||
```kotlin
|
Spezialisierte Serializer für Kotlin-Typen wie `Uuid`, `Instant` und `LocalDate`, um eine einheitliche JSON-Serialisierung über alle Services hinweg zu garantieren.
|
||||||
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<T>(
|
|
||||||
val data: T? = null,
|
|
||||||
val success: Boolean = true,
|
|
||||||
val message: String? = null,
|
|
||||||
val errors: List<String> = emptyList(),
|
|
||||||
val timestamp: Instant = Clock.System.now()
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PagedResponse<T>(
|
|
||||||
val content: List<T>,
|
|
||||||
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<DomainEvent>)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event Handler Interface
|
|
||||||
interface DomainEventHandler<T : DomainEvent> {
|
|
||||||
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<Uuid> {
|
|
||||||
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<Instant> {
|
|
||||||
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<LocalDate> {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core-Utils Komponenten
|
## Core-Utils Komponenten
|
||||||
|
|
||||||
### 1. Fehlerbehandlung (Result.kt)
|
### 1. Fehlerbehandlung (`Result.kt`)
|
||||||
|
Eine funktionale und typsichere `Result<T, E>`-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
|
### 3. Datenbank-Utilities (`DatabaseFactory.kt`, `DatabaseConfig.kt`)
|
||||||
sealed class Result<out T, out E> {
|
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.
|
||||||
data class Success<out T>(val value: T) : Result<T, Nothing>()
|
|
||||||
data class Failure<out E>(val error: E) : Result<Nothing, E>()
|
|
||||||
|
|
||||||
inline fun <R> map(transform: (T) -> R): Result<R, E> = when (this) {
|
### 4. Validierung (`ValidationUtils.kt`, `ApiValidationUtils.kt`)
|
||||||
is Success -> Success(transform(value))
|
Eine umfassende Sammlung von wiederverwendbaren Hilfsfunktionen zur Validierung von Daten, von einfachen Längenprüfungen bis hin zu komplexen API-Parameter-Validierungen.
|
||||||
is Failure -> this
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <R> flatMap(transform: (T) -> Result<R, E>): Result<R, E> = when (this) {
|
### 5. Service Discovery (`ServiceRegistration.kt`)
|
||||||
is Success -> transform(value)
|
Eine Implementierung zur Registrierung von Microservices bei einem Consul-Server, um eine dynamische Service-Landschaft zu ermöglichen.
|
||||||
is Failure -> this
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun mapError(transform: (E) -> E): Result<T, E> = 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 <T> Result<T, *>.onSuccess(action: (T) -> Unit): Result<T, *> {
|
|
||||||
if (this is Result.Success) action(value)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <E> 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<ValidationError> = emptyList()
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun valid() = ValidationResult(true)
|
|
||||||
fun invalid(errors: List<ValidationError>) = 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<String> = emptySet(),
|
|
||||||
val metadata: Map<String, String> = emptyMap()
|
|
||||||
)
|
|
||||||
|
|
||||||
interface ServiceRegistry {
|
|
||||||
suspend fun register(serviceInfo: ServiceInfo)
|
|
||||||
suspend fun deregister(serviceId: String)
|
|
||||||
suspend fun discover(serviceName: String): List<ServiceInfo>
|
|
||||||
suspend fun getHealthyServices(serviceName: String): List<ServiceInfo>
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ServiceInfo> {
|
|
||||||
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<ServiceInfo> {
|
|
||||||
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 <reified T> T.toJson(): String {
|
|
||||||
return JsonConfig.json.encodeToString(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T> String.fromJson(): T {
|
|
||||||
return JsonConfig.json.decodeFromString(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T> T.toPrettyJson(): String {
|
|
||||||
return JsonConfig.prettyJson.encodeToString(this)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verwendung in anderen Modulen
|
## Verwendung in anderen Modulen
|
||||||
|
|
||||||
### Domain Models
|
Andere Module deklarieren eine Abhängigkeit zum `core`-Modul, um auf dessen Bausteine zugreifen zu können.
|
||||||
|
|
||||||
```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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Responses
|
### API Responses
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// In API Controllers
|
// In API Controllers
|
||||||
@RestController
|
@RestController
|
||||||
@@ -584,16 +81,7 @@ class MemberController {
|
|||||||
|
|
||||||
@GetMapping("/api/members/{id}")
|
@GetMapping("/api/members/{id}")
|
||||||
fun getMember(@PathVariable id: String): ApiResponse<Member> {
|
fun getMember(@PathVariable id: String): ApiResponse<Member> {
|
||||||
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
|
// In Use Cases
|
||||||
class CreateMemberUseCase {
|
class CreateMemberUseCase {
|
||||||
suspend fun execute(member: Member): Result<Member, ValidationError> {
|
suspend fun execute(member: Member): Result<Member, ValidationError> {
|
||||||
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
|
## Best Practices
|
||||||
|
|
||||||
### 1. Shared Kernel Prinzipien
|
### 1. Shared Kernel Prinzipien
|
||||||
@@ -710,15 +108,13 @@ dependencies {
|
|||||||
|
|
||||||
### 2. Fehlerbehandlung
|
### 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
|
- **Validierung**: Frühe Validierung mit ValidationResult
|
||||||
- **Logging**: Strukturiertes Logging für Debugging
|
|
||||||
|
|
||||||
### 3. Serialisierung
|
### 3. Serialisierung
|
||||||
|
|
||||||
- **Custom Serializers**: Für spezielle Kotlin-Typen
|
- **Custom Serializers**: Für spezielle Datentypen werden die bereitgestellten Serializer verwendet, um Konsistenz zu gewährleisten.
|
||||||
- **Versionierung**: Schema-Evolution berücksichtigen
|
- **Schema-Evolution**: Bei der Weiterentwicklung von DTOs und Events muss die Abwärtskompatibilität berücksichtigt werden.
|
||||||
- **Performance**: Effiziente Serialisierung für häufige Operationen
|
|
||||||
|
|
||||||
## Zukünftige Erweiterungen
|
## 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).
|
Für weitere Informationen zur Gesamtarchitektur siehe [README.md](../README.md).
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.zaxxer.hikari.HikariDataSource
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory-Klasse für die Datenbankverbindung.
|
* Factory-Klasse für die Datenbankverbindung.
|
||||||
@@ -64,6 +65,30 @@ object DatabaseFactory {
|
|||||||
|
|
||||||
dataSource = HikariDataSource(hikariConfig)
|
dataSource = HikariDataSource(hikariConfig)
|
||||||
Database.connect(dataSource!!)
|
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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+100
-96
@@ -1,100 +1,104 @@
|
|||||||
package at.mocode.core.utils.database
|
package at.mocode.core.utils.database
|
||||||
|
|
||||||
import org.jetbrains.exposed.sql.*
|
/*
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
Wegen Flyway nicht mehr benötigt
|
||||||
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<Migration>()
|
|
||||||
private val executedMigrations = mutableSetOf<String>()
|
|
||||||
|
|
||||||
/**
|
//import org.jetbrains.exposed.sql.*
|
||||||
* Registriert eine Migration.
|
//import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
* @param migration Die zu registrierende Migration
|
//import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp
|
||||||
*/
|
//import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||||
fun register(migration: Migration) {
|
//
|
||||||
migrations.add(migration)
|
///**
|
||||||
}
|
// * Führt Datenbankmigrationen durch.
|
||||||
|
// * Diese Klasse verwaltet und führt alle notwendigen Datenbankmigrationen aus.
|
||||||
/**
|
// */
|
||||||
* Registriert mehrere Migrationen auf einmal.
|
//object DatabaseMigrator {
|
||||||
* @param migrations Die zu registrierenden Migrationen
|
// private val migrations = mutableListOf<Migration>()
|
||||||
*/
|
// private val executedMigrations = mutableSetOf<String>()
|
||||||
fun registerAll(vararg migrations: Migration) {
|
//
|
||||||
this.migrations.addAll(migrations)
|
// /**
|
||||||
}
|
// * Registriert eine Migration.
|
||||||
|
// * @param migration Die zu registrierende Migration
|
||||||
/**
|
// */
|
||||||
* Führt alle registrierten Migrationen aus, die noch nicht ausgeführt wurden.
|
// fun register(migration: Migration) {
|
||||||
*/
|
// migrations.add(migration)
|
||||||
fun migrate() {
|
// }
|
||||||
// Erstelle die Migrationstabelle, wenn sie nicht existiert
|
//
|
||||||
transaction {
|
// /**
|
||||||
SchemaUtils.create(MigrationTable)
|
// * Registriert mehrere Migrationen auf einmal.
|
||||||
|
// * @param migrations Die zu registrierenden Migrationen
|
||||||
// Lade bereits ausgeführte Migrationen
|
// */
|
||||||
MigrationTable.selectAll().forEach {
|
// fun registerAll(vararg migrations: Migration) {
|
||||||
executedMigrations.add(it[MigrationTable.id])
|
// this.migrations.addAll(migrations)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Sortiere Migrationen nach Version
|
// /**
|
||||||
val sortedMigrations = migrations.sortedBy { it.version }
|
// * Führt alle registrierten Migrationen aus, die noch nicht ausgeführt wurden.
|
||||||
|
// */
|
||||||
// Führe noch nicht ausgeführte Migrationen aus
|
// fun migrate() {
|
||||||
for (migration in sortedMigrations) {
|
// // Erstelle die Migrationstabelle, wenn sie nicht existiert
|
||||||
if (!executedMigrations.contains(migration.id)) {
|
// transaction {
|
||||||
println("Ausführen der Migration: ${migration.id}")
|
// SchemaUtils.create(MigrationTable)
|
||||||
try {
|
//
|
||||||
migration.up()
|
// // Lade bereits ausgeführte Migrationen
|
||||||
|
// MigrationTable.selectAll().forEach {
|
||||||
// Markiere Migration als ausgeführt
|
// executedMigrations.add(it[MigrationTable.id])
|
||||||
MigrationTable.insert {
|
// }
|
||||||
it[id] = migration.id
|
//
|
||||||
it[version] = migration.version
|
// // Sortiere Migrationen nach Version
|
||||||
it[description] = migration.description
|
// val sortedMigrations = migrations.sortedBy { it.version }
|
||||||
}
|
//
|
||||||
|
// // Führe noch nicht ausgeführte Migrationen aus
|
||||||
commit()
|
// for (migration in sortedMigrations) {
|
||||||
println("Migration erfolgreich: ${migration.id}")
|
// if (!executedMigrations.contains(migration.id)) {
|
||||||
} catch (e: Exception) {
|
// println("Ausführen der Migration: ${migration.id}")
|
||||||
rollback()
|
// try {
|
||||||
println("Migration fehlgeschlagen: ${migration.id} - ${e.message}")
|
// migration.up()
|
||||||
throw e
|
//
|
||||||
}
|
// // Markiere Migration als ausgeführt
|
||||||
}
|
// MigrationTable.insert {
|
||||||
}
|
// it[id] = migration.id
|
||||||
}
|
// it[version] = migration.version
|
||||||
}
|
// it[description] = migration.description
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// commit()
|
||||||
* Tabelle zur Verfolgung ausgeführter Migrationen.
|
// println("Migration erfolgreich: ${migration.id}")
|
||||||
*/
|
// } catch (e: Exception) {
|
||||||
object MigrationTable : Table("_migrations") {
|
// rollback()
|
||||||
val id = varchar("id", 100)
|
// println("Migration fehlgeschlagen: ${migration.id} - ${e.message}")
|
||||||
val version = long("version")
|
// throw e
|
||||||
val description = varchar("description", 255)
|
// }
|
||||||
val executedAt = timestamp("executed_at").defaultExpression(CurrentTimestamp)
|
// }
|
||||||
|
// }
|
||||||
override val primaryKey = PrimaryKey(id)
|
// }
|
||||||
}
|
// }
|
||||||
|
//}
|
||||||
/**
|
//
|
||||||
* Basisklasse für Datenbankmigrationen.
|
///**
|
||||||
*/
|
// * Tabelle zur Verfolgung ausgeführter Migrationen.
|
||||||
abstract class Migration(val version: Long, val description: String) {
|
// */
|
||||||
/**
|
//object MigrationTable : Table("_migrations") {
|
||||||
* Eindeutige ID der Migration, bestehend aus Version und Beschreibung.
|
// val id = varchar("id", 100)
|
||||||
*/
|
// val version = long("version")
|
||||||
val id: String = "V${version}_${description.replace("\\s+".toRegex(), "_")}"
|
// val description = varchar("description", 255)
|
||||||
|
// val executedAt = timestamp("executed_at").defaultExpression(CurrentTimestamp)
|
||||||
/**
|
//
|
||||||
* Führt die Migration aus.
|
// override val primaryKey = PrimaryKey(id)
|
||||||
*/
|
//}
|
||||||
abstract fun up()
|
//
|
||||||
}
|
///**
|
||||||
|
// * 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()
|
||||||
|
//}
|
||||||
|
|||||||
+33
@@ -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...
|
||||||
Reference in New Issue
Block a user