Bessere Möglichkeit, Kotlin-Datenobjekte Datenobjekten zuzuordnen


75

Ich möchte einige "Daten" -Klassenobjekte in ähnliche "Daten" -Klassenobjekte konvertieren / zuordnen. Zum Beispiel Klassen für Webformulare zu Klassen für Datenbankeinträge.

data class PersonForm(
    val firstName: String,
    val lastName: String,
    val age: Int,
    // maybe many fields exist here like address, card number, etc.
    val tel: String
)
// maps to ...
data class PersonRecord(
    val name: String, // "${firstName} ${lastName}"
    val age: Int, // copy of age
    // maybe many fields exist here like address, card number, etc.
    val tel: String // copy of tel
)

Ich verwende ModelMapper für solche Arbeiten in Java, aber es kann nicht verwendet werden, da Datenklassen endgültig sind (ModelMapper erstellt CGLib-Proxys zum Lesen von Zuordnungsdefinitionen). Wir können ModelMapper verwenden, wenn wir diese Klassen / Felder öffnen, aber wir müssen Funktionen der Klasse "data" manuell implementieren. (Siehe ModelMapper-Beispiele: https://github.com/jhalterman/modelmapper/blob/master/examples/src/main/java/org/modelmapper/gettingstarted/GettingStartedExample.java )

Wie ordne ich solche "Daten" -Objekte in Kotlin zu?

Update: ModelMapper ordnet automatisch gleichnamige Felder (wie tel -> tel) ohne Mapping-Deklarationen zu. Ich möchte es mit der Datenklasse von Kotlin machen.

Update: Der Zweck jeder Klasse hängt von der Art der Anwendung ab, diese werden jedoch wahrscheinlich in der verschiedenen Ebene einer Anwendung platziert.

Zum Beispiel:

  • Daten aus der Datenbank (Datenbankentität) zu Daten für das HTML-Formular (Modell / Ansichtsmodell)
  • REST-API-Ergebnis zu Daten für die Datenbank

Diese Klassen sind ähnlich, aber nicht gleich.

Ich möchte normale Funktionsaufrufe aus folgenden Gründen vermeiden:

  • Dies hängt von der Reihenfolge der Argumente ab. Eine Funktion für eine Klasse mit vielen Feldern desselben Typs (wie String) kann leicht beschädigt werden.
  • Viele Deklarationen sind notwendig, obwohl die meisten Zuordnungen mit Namenskonventionen aufgelöst werden können.

Natürlich ist eine Bibliothek mit ähnlichen Funktionen vorgesehen, aber auch Informationen über die Kotlin-Funktion sind willkommen (wie das Verbreiten in ECMAScript).


Bitte beschreiben Sie, wie Sie die zugeordneten Klassen verwenden möchten. Was ist der Zweck von zwei getrennten Datenformaten?
— Voddan

Ich habe noch nie von einer Duplizierung des Datenmodells gehört (außer in Fällen von Legacy-Code). Normalerweise sind die Daten , die Sie arbeiten mit (Ansicht Modell) ist die Daten , die Sie in die Datenbank gestellt.
— Voddan

@voddan Ein Anwendungsfall wäre, nur Teile des Domänenmodells verschiedenen API-Verbrauchern zur Verfügung zu stellen. Ein separates DTO pro Ansicht des Domänenmodells ist viel sauberer als die Verwendung von JsonView IMHO
— miensol

Benötigen Sie diese Klassen wirklich als Datenklassen?
— Alexey Andreev

Antworten:


65
  1. Am einfachsten (am besten?):

    fun PersonForm.toPersonRecord() = PersonRecord(
            name = "$firstName $lastName",
            age = age,
            tel = tel
    )
    
  2. Reflexion (keine großartige Leistung):

    fun PersonForm.toPersonRecord() = with(PersonRecord::class.primaryConstructor!!) {
        val propertiesByName = PersonForm::class.memberProperties.associateBy { it.name }
        callBy(args = parameters.associate { parameter ->
            parameter to when (parameter.name) {
                "name" -> "$firstName $lastName"
                else -> propertiesByName[parameter.name]?.get(this@toPersonRecord)
            }
        })
    }
    
  3. Zwischengespeicherte Reflexion (okay Leistung, aber nicht so schnell wie # 1):

    open class Transformer<in T : Any, out R : Any>
    protected constructor(inClass: KClass<T>, outClass: KClass<R>) {
        private val outConstructor = outClass.primaryConstructor!!
        private val inPropertiesByName by lazy {
            inClass.memberProperties.associateBy { it.name }
        }
    
        fun transform(data: T): R = with(outConstructor) {
            callBy(parameters.associate { parameter ->
                parameter to argFor(parameter, data)
            })
        }
    
        open fun argFor(parameter: KParameter, data: T): Any? {
            return inPropertiesByName[parameter.name]?.get(data)
        }
    }
    
    val personFormToPersonRecordTransformer = object
    : Transformer<PersonForm, PersonRecord>(PersonForm::class, PersonRecord::class) {
        override fun argFor(parameter: KParameter, data: PersonForm): Any? {
            return when (parameter.name) {
                "name" -> with(data) { "$firstName $lastName" }
                else -> super.argFor(parameter, data)
            }
        }
    }
    
    fun PersonForm.toPersonRecord() = personFormToPersonRecordTransformer.transform(this)
    
  4. Speichern von Eigenschaften in einer Karte

    data class PersonForm(val map: Map<String, Any?>) {
        val firstName: String   by map
        val lastName: String    by map
        val age: Int            by map
        // maybe many fields exist here like address, card number, etc.
        val tel: String         by map
    }
    
    // maps to ...
    data class PersonRecord(val map: Map<String, Any?>) {
        val name: String    by map // "${firstName} ${lastName}"
        val age: Int        by map // copy of age
        // maybe many fields exist here like address, card number, etc.
        val tel: String     by map // copy of tel
    }
    
    fun PersonForm.toPersonRecord() = PersonRecord(HashMap(map).apply {
        this["name"] = "${remove("firstName")} ${remove("lastName")}"
    })
    

2
MapStruct mit @KotlinBuilderist eine schöne und schnelle Lösung. Siehe andere Antwort (wo ich @KotlinBuilderInformationen hinzugefügt habe ).
— Arberg

2
Eigentlich habe ich MapStruct jetzt nicht mehr verwendet und gehe einfach mit der ersten erwähnten Lösung, dh. manuelle Zuordnung. MapStruct basiert auf Java und bietet daher keine Null-Sicherheit. Außerdem finde ich, dass ich mit dem einfachen Kotlin-Initialisierer ohnehin Sicherheit zur Kompilierungszeit bekomme, wenn der Konstruktor keine Standardwerte enthält. Wenn also ein Feld zum Datenobjekt hinzugefügt wird, wird ein Kompilierungsfehler angezeigt, den ich möchte.
— Arberg

20

Suchen Sie das?

data class PersonRecord(val name: String, val age: Int, val tel: String){       
    object ModelMapper {
        fun from(form: PersonForm) = 
            PersonRecord(form.firstName + form.lastName, form.age, form.tel)           
    }
}

und dann:

val personRecord = PersonRecord.ModelMapper.from(personForm)

5
Die Operation, die Sie schreiben, wollte ich machen. Ich möchte jedoch Mapping-Deklarationen reduzieren, da viele Felder mit demselben Namen (wie tel -> tel) vorhanden sind. Ich möchte nur spezielle Regeln wie Vorname + Nachname => Name schreiben.
— Sunnyone

3
Der Vorschlag für eine Dataarg / Named Spread-Konvention könnte hierfür hilfreich sein. youtrack.jetbrains.com/issue/KT-15471
— Tom Power

13

Mit MapStruct kann kapt Klassen generieren, die das Mapping durchführen (ohne Reflexion).

Verwenden Sie MapStruct:

@Mapper
interface PersonConverter {

    @Mapping(source = "phoneNumber", target = "phone")
    fun convertToDto(person: Person) : PersonDto

    @InheritInverseConfiguration
    fun convertToModel(personDto: PersonDto) : Person

}


// Note this either needs empty constructor or we need @KotlinBuilder as dsecribe below
data class Person: this(null, null, null, null) (...)

Verwenden:

val converter = Mappers.getMapper(PersonConverter::class.java) // or PersonConverterImpl()

val person = Person("Samuel", "Jackson", "0123 334466", LocalDate.of(1948, 12, 21))

val personDto = converter.convertToDto(person)
println(personDto)

val personModel = converter.convertToModel(personDto)
println(personModel)

Bearbeiten:

Jetzt mit @KotlinBuilder, um das Problem constructor () zu vermeiden:

GitHub: Pozos Mapstruct-Kotlin

Kommentieren Sie Datenklassen mit @KotlinBuilder. Dadurch wird eine PersonBuilderKlasse erstellt, die MapStruct verwendet. Daher vermeiden wir, die Schnittstelle der Datenklasse mit einem Konstruktor () zu ruinieren.

@KotlinBuilder
data class Person(
    val firstName: String,
    val lastName: String,
    val age: Int,
    val tel: String
)

Abhängigkeit:

// https://mvnrepository.com/artifact/com.github.pozo/mapstruct-kotlin
api("com.github.pozo:mapstruct-kotlin:1.3.1.1")
kapt("com.github.pozo:mapstruct-kotlin-processor:1.3.1.1")

https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-kotlin


3
Sie haben vergessen, "winzige" Details zu erwähnen. Ihre Datenklassen müssen veränderbar sein und Sie müssen einen benutzerdefinierten Konstruktor von haben constructor() : this(null, null, null, null). Bis das mapstruct-Team die richtige Kotlin-Unterstützung gefunden hat, würde ich diese vermeiden und die manuelle Konvertierung durchführen, wie in seiner ersten Lösung @ mfulton26 erwähnt.
— randomUser56789

Ich würde MapStruct auch empfehlen, da es keine Reflexion verwendet und daher schneller ist. Für das Konstruktorproblem können Sie diese Erweiterung github.com/Pozo/mapstruct-kotlin verwenden , mit der Sie den Builder-Ansatz verwenden können, für den der Konstruktor () nicht mehr erforderlich ist: dieses (null, null) Fiasko
— FearlessHyena

4

Willst du wirklich eine eigene Klasse dafür? Sie können der ursprünglichen Datenklasse Eigenschaften hinzufügen:

data class PersonForm(
    val firstName: String,
    val lastName: String,
    val age: Int,
    val tel: String
) {
    val name = "${firstName} ${lastName}"
}

1
Eine Spearate-Klasse ist nicht unbedingt erforderlich. Aber ich möchte vermeiden, abhängig von der Reihenfolge der Argumente.
— Sunnyone

3
@sunnyone Sie können beim Erstellen Ihrer Datenobjekte immer benannte Argumente verwenden, damit Sie nicht von der Reihenfolge abhängen, in der die Argumente definiert sind.
— mfulton26

3

Dies funktioniert mit Gson:

inline fun <reified T : Any> Any.mapTo(): T =
    GsonBuilder().create().run {
        toJson(this@mapTo).let { fromJson(it, T::class.java) }
    }

fun PersonForm.toRecord(): PersonRecord =
    mapTo<PersonRecord>().copy(
        name = "$firstName $lastName"
    )

fun PersonRecord.toForm(): PersonForm =
    mapTo<PersonForm>().copy(
        firstName = name.split(" ").first(),
        lastName = name.split(" ").last()
    )

mit nicht nullable Werte null sein darf , weil Gson verwendet sun.misc.Unsafe ..


1
Gson ist ziemlich träge
— AFD

3

Verwenden von ModelMapper

/** Util.kt **/

class MapperDto() : ModelMapper() {
    init {
        configuration.matchingStrategy = MatchingStrategies.LOOSE
        configuration.fieldAccessLevel = Configuration.AccessLevel.PRIVATE
        configuration.isFieldMatchingEnabled = true
        configuration.isSkipNullEnabled = true
    }
}

object Mapper {
    val mapper = MapperDto()

    inline fun <S, reified T> convert(source: S): T = mapper.map(source, T::class.java)
}

Verwendung

val form = PersonForm(/** ... **/)
val record: PersonRecord = Mapper.convert(form)

Möglicherweise benötigen Sie einige Zuordnungsregeln, wenn sich die Feldnamen unterscheiden. Siehe Erste Schritte
PS: Verwenden Sie das kotlin no-argsPlugin, um einen Standardkonstruktor ohne Argumente für Ihre Datenklassen zu verwenden


1

Mit ModelMapper können Sie eine Kotlin-Datenklasse zuordnen. Die Schlüssel sind:

  • Verwenden Sie @JvmOverloads (generiert einen Konstruktor ohne Argumente)
  • Standardwerte für Datenklassenmitglied
  • Veränderliches Mitglied, var statt val

    data class AppSyncEvent @JvmOverloads constructor(
        var field: String = "",
        var arguments: Map<String, *> = mapOf<String, Any>(),
        var source: Map<String, *> = mapOf<String, Any>()
    )
    
    val event = ModelMapper().map(request, AppSyncEvent::class.java)
    

1

Für ModelMapper können Sie das No- Arg -Compiler-Plugin von Kotlin verwenden , mit dem Sie eine Annotation erstellen können, die Ihre Datenklasse markiert, um einen synthetischen No-Arg-Konstruktor für Bibliotheken zu erhalten, die Reflection verwenden. Ihre Datenklasse muss varanstelle von verwenden val.

package com.example

annotation class NoArg

@NoArg
data class MyData(var myDatum: String)

mm.map(. . ., MyData::class.java)

und in build.gradle (siehe Dokumente für Maven):

buildscript {
  . . .
  dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
  }
}

apply plugin: 'kotlin-noarg'

noArg {
  annotation "com.example.NoArg"
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.