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


75

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

我将ModelMapper用于Java中的此类工作,但由于数据类是最终的,因此无法使用(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结果转换为数据库数据

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

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

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

当然,可以使用具有类似功能的库,但是也欢迎使用Kotlin功能的信息(例如在ECMAScript中传播)。


请描述您如何使用映射的类。具有两种不同的数据格式的目的是什么?
voddan

从未听说过重复数据模型(遗留代码的情况除外)。通常,您正在使用的数据(视图模型)您放入数据库中的数据。
voddan

@voddan一种用例是仅将域模型的一部分公开给不同的API使用者。每个域模型视图具有单独的DTO比使用JsonView IMHO更加干净
miensol

您真的需要这些类作为数据类吗?
阿列克谢·安德列夫

Answers:


67
  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
MapStruct with@KotlinBuilder是一种美观,快速的解决方案。查看其他答案(我在其中添加了@KotlinBuilder信息)。
arberg

2
实际上,我现在已经停止使用MapStruct了,只使用提到的第一个解决方案即可。手动映射。MapStruct是基于Java的,因此不提供null安全性。我也发现在构造函数上没有默认值的情况下,无论如何,我都可以通过简单的kotlin初始化程序获得编译时的安全性。因此,如果将字段添加到数据对象上,则会出现编译错误,这正是我想要的。
Arberg

20

您在找这个吗?

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)

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

14

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来避免builder()问题:

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


3
您忘记提及“微小的”细节。您的数据类必须是可变的,并且必须具有的自定义构造函数 constructor() : this(null, null, null, null)。因此,在mapstruct团队提供适当的kotlin支持之前,我会避免使用它,而是像他在第一个解决方案中提到的@ mfulton26那样进行手动转换。
randomUser56789

我还建议使用MapStruct,因为它不使用反射,因此速度更快。对于构造问题,你可以使用这个扩展github.com/Pozo/mapstruct-kotlin这个(NULL,NULL)惨败了:它允许您使用Builder方法不需要构造函数()
FearlessHyena

4

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

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

1
不需要穿刺课程。但我想避免取决于参数的顺序。
2016年

3
@sunnyone在构造数据对象时,您始终可以使用命名参数,这样您就不必依赖于定义参数的顺序
。– mfulton26

4

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

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


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

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


1
格森(Gson)表现呆滞
AFD

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)
    

1

对于ModelMapper,您可以使用Kotlin的no-arg编译器插件,使用该插件可以创建标记您的数据类的注释,以获取使用反射的库的合成no-arg构造函数。您的数据类需要使用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"
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.