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

sun*_*one 50 java modelmapper 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
)
Run Code Online (Sandbox Code Playgroud)

我在Java中使用ModelMapper进行此类工作,但由于数据类是final(ModelMapper创建CGLib代理以读取映射定义),因此无法使用它.当我们打开这些类/字段时,我们可以使用ModelMapper,但是我们必须手动实现"data"类的功能.(参见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中传播).

mfu*_*n26 43

  1. 最简单(最好的?):

    fun PersonForm.toPersonRecord() = PersonRecord(
            name = "$firstName $lastName",
            age = age,
            tel = tel
    )
    
    Run Code Online (Sandbox Code Playgroud)
  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)
            }
        })
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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)
    
    Run Code Online (Sandbox Code Playgroud)
  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")}"
    })
    
    Run Code Online (Sandbox Code Playgroud)

  • 实际上我现在已经停止使用 MapStruct,只是使用提到的第一个解决方案,即。手动映射。MapStruct是基于java的,所以不提供空安全。我还发现,当构造函数没有默认值时,我可以通过简单的 kotlin 初始化器获得编译时安全性。因此,如果在数据对象上添加字段,我会收到编译错误,这正是我想要的。 (4认同)
  • 带有“@KotlinBuilder”的 MapStruct 是一个美观且快速的解决方案。请参阅其他答案(我在其中添加了“@KotlinBuilder”信息)。 (3认同)

kli*_*mat 16

这是你在找?

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)           
    }
}
Run Code Online (Sandbox Code Playgroud)

然后:

val personRecord = PersonRecord.ModelMapper.from(personForm)
Run Code Online (Sandbox Code Playgroud)

  • 你写的操作是我想做的.但我想减少映射声明,因为存在许多具有相同名称的字段(如tel - > tel).我只想写一些特殊的规则,比如firstName + lastName => name. (4认同)
  • dataarg /命名传播约定提案可能对此有用吗?https://youtrack.jetbrains.com/issue/KT-15471 (2认同)

小智 6

使用MapStruct:

@Mapper
interface PersonConverter {

    @Mapping(source = "phoneNumber", target = "phone")
    fun convertToDto(person: Person) : PersonDto

    @InheritInverseConfiguration
    fun convertToModel(personDto: PersonDto) : Person

}
Run Code Online (Sandbox Code Playgroud)

使用:

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)
Run Code Online (Sandbox Code Playgroud)

https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-kotlin

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

zac*_*ein 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)
}
Run Code Online (Sandbox Code Playgroud)

用法

val form = PersonForm(/** ... **/)
val record: PersonRecord = Mapper.convert(form)
Run Code Online (Sandbox Code Playgroud)

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