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).

Sunnyone
quelle
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")}"
    })
    
mfulton26
quelle
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)
klimat
quelle
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

Евгений Иванов
quelle
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}"
}
Voddan
quelle
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 ..

Tom Power
quelle
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

Zack Evein
quelle
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)
    
Ken
quelle
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"
}
Mike Placentra
quelle