93

我想将一些“数据”类对象转换/映射为类似的“数据”类对象。例如,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 中传播)。

4

8 回答 8

81
  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")}"
    })
    
于 2016-08-29T19:25:12.777 回答
25

这是你要找的吗?

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)
于 2016-08-29T08:31:02.107 回答
16

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 的 mapstruct-kotlin

用 注释数据类@KotlinBuilder。这将创建一个 PersonBuilderMapStruct 使用的类,因此我们避免使用 constructor() 破坏数据类的接口。

@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

于 2018-06-27T05:46:28.497 回答
5

使用模型映射器

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

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

于 2019-09-02T13:07:54.033 回答
4

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

data class PersonForm(
    val firstName: String,
    val lastName: String,
    val age: Int,
    val tel: String
) {
    val name = "${firstName} ${lastName}"
}
于 2016-08-29T13:30:56.713 回答
3

这使用 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 ..

于 2018-03-12T10:57:12.067 回答
1

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

  • 使用@JvmOverloads(生成不带参数的构造函数)
  • 数据类成员的默认值
  • 可变成员,var 而不是 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)
    
于 2018-08-01T18:48:51.417 回答
1

对于 ModelMapper,您可以使用Kotlin 的无参数编译器插件,您可以使用它创建一个注释来标记您的数据类,以获得使用反射的库的合成无参数构造函数。您的数据类需要使用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"
}
于 2019-07-01T15:06:56.287 回答