ChatGPT解决这个技术问题 Extra ChatGPT

将 Kotlin 数据对象映射到数据对象的更好方法

我想将一些“数据”类对象转换/映射为类似的“数据”类对象。例如,Web 表单类到数据库记录类。

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
)

我在 Java 中使用 ModelMapper 进行此类工作,但它不能使用,因为数据类是最终的(ModelMapper 创建 CGLib 代理来读取映射定义)。当我们打开这些类/字段时,我们可以使用 ModelMapper,但我们必须手动实现“数据”类的功能。 (参见 ModelMapper 示例:https://github.com/jhalterman/modelmapper/blob/master/examples/src/main/java/org/modelmapper/gettingstarted/GettingStartedExample.java

如何在 Kotlin 中映射这样的“数据”对象?

更新:ModelMapper 自动映射具有相同名称的字段(如 tel -> tel),无需映射声明。我想用 Kotlin 的数据类来做。

更新:每个类的用途取决于什么样的应用程序,但这些可能被放置在应用程序的不同层中。

例如:

从数据库(数据库实体)到 HTML 表单数据(模型/视图模型)的数据

REST API 结果到数据库的数据

这些类是相似的,但并不相同。

由于以下原因,我想避免正常的函数调用:

这取决于参数的顺序。具有许多具有相同类型(如字符串)的字段的类的函数将很容易被破坏。

尽管大多数映射可以通过命名约定来解决,但许多声明是必要的。

当然,想要一个具有类似功能的库,但也欢迎 Kotlin 功能的信息(如在 ECMAScript 中传播)。

请描述您希望如何使用映射的类。拥有两种不同的数据格式的目的是什么?
从未听说过重复数据模型(遗留代码除外)。通常,您正在使用的数据(视图模型)是您放入数据库的数据。
@voddan 一个用例是仅向不同的 API 使用者公开域模型的一部分。每个域模型的 view 都有单独的 DTO 比使用 ie JsonView 恕我直言要干净得多
你真的需要这些类作为数据类吗?

m
mfulton26

最简单的(最好的?):fun PersonForm.toPersonRecord() = PersonRecord( name = "$firstName $lastName", age = age, tel = tel ) 反射(不是很好的表现):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) } }) } 缓存反射(性能不错,但不如 #1 快):open class Transformer protected constructor(inClass: KClass, outClass: KClass) { private val outConstructor = outClass.primaryConstructor!! private val inPropertiesByName bylazy { inClass.memberProperties.associateBy { it.name } } 有趣的变换(数据:T):R = with(outConstructor) { callBy(parameters.associate { 参数 -> 参数到 argFor(参数,数据)} ) } open fun argFor(parameter: KParameter, data: T): Any? { return inPropertiesByName[parameter.name]?.get(data) } } val personFormToPersonRecordTransformer = object : Transformer(PersonForm::class, PersonRecord::class) { override fun argFor(parameter: KParameter, data: PersonForm ): 任何? { return when (parameter.name) { "name" -> with(data) { "$firstName $lastName" } else -> super.argFor(parameter, data) } } } fun PersonForm.toPersonRecord() = personFormToPersonRecordTransformer.transform (this) 在 Map 数据类中存储属性 PersonForm(val map: Map) { val firstName: String by map val lastName: String by map val age: Int by map // 这里可能存在很多字段,例如地址、卡号等 val tel: String by map } // 映射到 ... data class PersonRecord(val map: Map) { val name: String by map // "${firstName} ${lastName}" val age: Int by map // 年龄副本 // 这里可能存在很多字段,例如地址、卡号等 val tel: String by map // tel 副本 } fun PersonForm.toPersonRecord() = PersonRecord(HashMap(map).apply { this["name"] = "${remove("firstName")} ${remove("lastName")}" })


带有 @KotlinBuilder 的 MapStruct 是一种美观且快速的解决方案。查看其他答案(我在其中添加了 @KotlinBuilder 信息)。
实际上我现在已经停止使用 MapStruct,只使用提到的第一个解决方案,即。手动映射。 MapStruct 是基于 java 的,因此不提供空值安全性。此外,当构造函数上没有默认值时,我发现无论如何我都可以使用简单的 kotlin 初始化程序获得编译时安全性。所以如果在数据对象上添加一个字段,我会得到一个编译错误,这就是我想要的。
k
klimat

这是你要找的吗?

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

接着:

val personRecord = PersonRecord.ModelMapper.from(personForm)

你写的操作是我想做的。但我想减少映射声明,因为存在许多具有相同名称(如 tel -> tel)的字段。我只想写一些特殊的规则,比如 firstName + lastName => name。
a
arberg

MapStruct 让 kapt 生成进行映射的类(没有反射)。

使用 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) (...)

利用:

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)

编辑:

现在使用 @KotlinBuilder 来避免 constructor() 问题:

GitHub: Pozo's mapstruct-kotlin

使用 @KotlinBuilder 注释数据类。这将创建 MapStruct 使用的 PersonBuilder 类,因此我们避免使用构造函数()破坏数据类的接口。

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

依赖:

// 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


你忘了提到“微小”的细节。您的数据类必须是可变的,并且您必须有 constructor() : this(null, null, null, null) 的自定义构造函数。因此,在 mapstruct 团队提供适当的 kotlin 支持之前,我会避免使用它并像@mfulton26 在他的第一个解决方案中提到的那样进行手动转换。
我还推荐 MapStruct,因为它不使用反射,因此速度更快。对于构造函数问题,您可以使用此扩展 github.com/Pozo/mapstruct-kotlin,它允许您使用不需要 constructor() 的构建器方法:this(null, null) 失败了
z
zack evein

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

用法

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

如果字段名称不同,您可能需要一些映射规则。请参阅 the getting started
PS:使用 kotlin no-args 插件为您的数据类提供默认的无参数构造函数


v
voddan

你真的想要一个单独的课程吗?您可以将属性添加到原始数据类:

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

不需要单独的类。但我想避免取决于参数的顺序。
@sunnyone 您在构造数据对象时始终可以使用 named arguments ,这样您就不必依赖于定义参数的顺序。
T
Tom Power

这使用 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()
    )

不可为空的值允许为空,因为 Gson 使用 sun.misc.Unsafe..


Gson 很慢
K
Ken

您可以使用 ModelMapper 映射到 Kotlin 数据类。关键是:

使用@JvmOverloads(生成不带参数的构造函数)

数据类成员的默认值

可变成员,var 代替 val 数据类 AppSyncEvent @JvmOverloads 构造函数(var field: String = "", var arguments: Map = mapOf(), var source: Map = mapOf() ) val 事件 = ModelMapper().map(request, AppSyncEvent::class.java)


M
Mike Placentra

对于 ModelMapper,您可以使用 Kotlin's no-arg compiler plugin,您可以使用它创建一个注释来标记您的数据类,以获得使用反射的库的合成无参数构造函数。您的数据类需要使用 var 而不是 val

package com.example

annotation class NoArg

@NoArg
data class MyData(var myDatum: String)

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

在 build.gradle 中(参见 Maven 文档):

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

apply plugin: 'kotlin-noarg'

noArg {
  annotation "com.example.NoArg"
}