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:
Ces classes sont similaires, mais ne sont pas les mêmes.
Je veux éviter les appels de fonction normaux pour ces raisons:
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).
Le plus simple (le meilleur?):
fun PersonForm.toPersonRecord() = PersonRecord(
name = "$firstName $lastName",
age = age,
tel = tel
)
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)
}
})
}
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)
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")}"
})
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)
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}"
}
Vous pouvez utiliser ModelMapper pour mapper vers une classe de données Kotlin. Les clés sont:
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)
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
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 ..
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"
}