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

2022-08-31 10:45:20

我想将一些“数据”类对象转换/映射到类似的“数据”类对象。例如,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,但是我们必须手动实现“data”类的功能。(参见模型映射器示例:https://github.com/jhalterman/modelmapper/blob/master/examples/src/main/java/org/modelmapper/gettingstarted/GettingStartedExample.java)

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

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

更新:每个类的用途取决于应用程序的种类,但这些类可能位于应用程序的不同层中。

例如:

  • 数据从数据库(数据库实体)到 HTML 表单的数据(模型/视图模型)
  • 数据库数据的 REST API 结果

这些类相似,但不相同。

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

  • 这取决于参数的顺序。具有许多具有相同类型(如 String)的字段的类的函数将很容易被破坏。
  • 许多声明都是必需的,尽管大多数映射都可以通过命名约定来解决。

当然,具有类似功能的库是有意的,但 Kotlin 功能的信息也是受欢迎的(就像在 ECMAScript 中传播一样)。


答案 1
  1. 最简单(最好?

    fun PersonForm.toPersonRecord() = PersonRecord(
            name = "$firstName $lastName",
            age = age,
            tel = tel
    )
    
  2. 反射(性能不是很好):

    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. 缓存反射(性能不错,但不如 #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. 在映射中存储属性

    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

这是你在寻找吗?

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)

推荐