# DTOs
Meistens entsprechen die Entities nur zum Teil den Objekten, die über APIs ausgetauscht werden. Die API-Objekte enthalten oft nur eine Untermenge der Entity-Properties, da nicht alles für die API generell oder je API-Funktion relevant ist. Bestimmte Properties können beim Schreiben oder Lesen via API weggelassen werden.
Um dies zu realisieren, müssen entsprechende Klassen definiert und diese dann zu einem Entity oder von diesem umgewandelt (gemappt) werden. Diese spezielle Objekte, die zum Datenaustausch dienen, nennt man DTOs (Data Transfer Objects).
# Objekte
Für das Umwandeln werden die nötigen Hilfsmethoden geschrieben, die den einen Typ in den entsprechenden anderen Typ umwandeln. Diese können direkt in den DTOs enthalten sein.
Hier ist ein Beispiel, bei dem die DTOs als normale Klassen definiert wurden. Das erspart Code, wenn diese sich nur marginal unterscheiden (z. B. falls das Lese-Objekt im Vergleich zum Schreibe-Objekt als Zusatz nur id enthält). Trotzdem sollte dies so nur in wirklich sehr einfachen Fällen eingesetzt werden.
@Entity
class User(
var name: String
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0
}
class GetUserDto: CreateUserDto() {
var id: Long = 0
}
class CreateUserDto {
var name: String = ""
}
Wird es etwas komplexer, manchmal auch mit Unterschieden in tieferen Objektstrukturen, sollten für Lesen und Schreiben (und sonstige Vorgänge) eigenständige DTOs definiert werden. Dafür können auch data classes benutzt werden.
data class GetDashboardDto(
val id: Long, // id ist immer vorhanden
val title: String,
val rows: List<Row> // rows ist immer vorhanden, und wenn es nur ein leeres Array ist
) {
@Schema(name = "GetDashboardDto_Row")
data class Row(
val boxes: List<Box> // boxes ist immer vorhanden, und wenn es nur ein leeres Array ist
) {
@Schema(name = "GetDashboardDto_Row_Box")
data class Box(
val id: Long, // eine verknüpfte box hat immer eine id
val title: String
)
}
}
data class CreateDashboardDto(
// keine id, da neues Objekt erzeugt wird
val title: String,
val rows: List<Row>? // rows ist optional
) {
@Schema(name = "CreateDashboardDto_Row")
data class Row(
val boxes: List<Box>? // boxes ist optional
) {
@Schema(name = "CreateDashboardDto_Row_Box")
data class Box(
@Schema(description = "null, if a new box is to be created")
val id: Long?, // entweder bereits bestehende box, oder neue, wenn null
val title: String
)
}
}
# Umwandeln
Zum Umwandeln der DTOs in Entities und umgekehrt, sollte eine Converter-@Component Klasse angelegt werden, die dann an den entsprechenden Stellen (also in der API) als Bean angefordert wird. Der Converter kann dann wiederum die Services anfordern, die er benötigt (fast immer den Service der Hauptentity, für manche Operationen aber vielleicht auch Services von Unter-Entities).
@Component
class UserConverter(
private val userService: UserService
) {
fun toGetUserDto(entity: User) = GetUserDto().apply {
id = entity.id
name = entity.name
}
fun toEntity(dto: CreateUserDto, baseEntity: User? = null): User {
val entity = baseEntity ?: User()
entity.name = dto.name
return entity
}
}
@Component
class DashboardConverter(
private val userService: DashboardService
) {
fun toGetDashboardDto(entity: Dashboard): GetDashboardDto {
// ...
}
fun toEntity(dto: CreateDashboardDto, baseEntity: Dashboard? = null): Dashboard {
val entity = baseEntity ?: Dashboard(dto.title)
entity.title = dto.title
entity.rows.clear()
if (dto.rows != null) {
// ...
}
}
}
Beim DashboardConverter sieht, dass es ein paar Abfragen mehr gibt, da im DTO einige Properties null sein können.