web-dev-qa-db-fra.com

Meilleure façon de mapper des objets de données Kotlin à des objets de données

Je souhaite convertir/mapper des objets de classe "données" en objets de classe "données" similaires. Par exemple, des classes de formulaire Web en classes pour des enregistrements de base de données.

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
)

J'utilise ModelMapper pour de tels travaux en Java, mais cela ne peut pas être utilisé car les classes de données sont définitives (ModelMapper crée des proxy CGLib pour lire les définitions de mappage). Nous pouvons utiliser ModelMapper lorsque nous ouvrons ces classes/champs, mais nous devons implémenter les fonctionnalités de la classe "data" manuellement. (voir les exemples de ModelMapper: https://github.com/jhalterman/modelmapper/blob/master/examples/src/main/Java/org/modelmapper/gettingstarted/GettingStartedExample.Java )

Comment mapper de tels objets "données" dans Kotlin?

Mise à jour: ModelMapper mappe automatiquement les champs ayant le même nom (comme tel -> tel) sans déclaration de mappage. Je veux le faire avec la classe de données de Kotlin.

Mise à jour: Le but de chaque classe dépend du type d'application, mais celles-ci sont probablement placées dans les différentes couches d'une application.

Par exemple:

  • données de la base de données (entité de base de données) aux données pour le formulaire HTML (modèle/modèle de vue)
  • Résultat de l'API REST en données pour la base de données

Ces classes sont similaires, mais ne sont pas les mêmes.

Je veux éviter les appels de fonction normaux pour ces raisons:

  • Cela dépend de l'ordre des arguments. Une fonction pour une classe avec plusieurs champs ayant le même type (comme String) sera facilement cassée.
  • De nombreuses déclarations sont nécessaires bien que la plupart des mappages puissent être résolus avec une convention de nommage.

Bien entendu, une bibliothèque ayant une fonctionnalité similaire est prévue, mais des informations sur la fonctionnalité Kotlin sont également les bienvenues (comme la diffusion dans ECMAScript).

45
sunnyone
  1. Le plus simple (le meilleur?):

    fun PersonForm.toPersonRecord() = PersonRecord(
            name = "$firstName $lastName",
            age = age,
            tel = tel
    )
    
  2. Réflexion (pas très bonne performance):

    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. Réflexion en cache (performance correcte mais pas aussi rapide que la n ° 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. Stockage des propriétés dans une carte

    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")}"
    })
    
37
mfulton26

Est-ce que vous cherchez?

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

et alors:

val personRecord = PersonRecord.ModelMapper.from(personForm)
15
klimat

Utilisez MapStruct:

@Mapper
interface PersonConverter {

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

    @InheritInverseConfiguration
    fun convertToModel(personDto: PersonDto) : Person

}

Utilisation:

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)

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

Voulez-vous vraiment un cours séparé pour cela? Vous pouvez ajouter des propriétés à la classe de données d'origine:

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

Vous pouvez utiliser ModelMapper pour mapper vers une classe de données Kotlin. Les clés sont:

  • Utilisez @JvmOverloads (génère un constructeur sans argument)
  • Valeurs par défaut pour le membre de la classe de données
  • Membre mutable, var au lieu de 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)
    
0
Ken

Utilisation de 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)
}

Usage

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

Vous aurez peut-être besoin de règles de mappage si les noms de champs diffèrent. Voir la mise en route
PS: Utilisez kotlin no-args plugin pour avoir le constructeur sans argument par défaut avec vos classes de données

0
zack evein

Cela fonctionne avec 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()
    )

avec des valeurs non Nullables, Null étant autorisé car Gson utilise Sun.misc.Unsafe ..

0
Tom Power

Pour ModelMapper, vous pouvez utiliser plug-in du compilateur no-arg de Kotlin , avec lequel vous pouvez créer une annotation qui marque votre classe de données afin d'obtenir un constructeur synthétique sans argument pour les bibliothèques qui utilisent la réflexion. Votre classe de données doit utiliser var au lieu de val.

package com.example

annotation class NoArg

@NoArg
data class MyData(var myDatum: String)

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

et dans build.gradle (voir la documentation de Maven):

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

apply plugin: 'kotlin-noarg'

noArg {
  annotation "com.example.NoArg"
}
0
Mike Placentra