我想将一些“数据”类对象转换/映射为类似的“数据”类对象。例如,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 中传播)。
最简单的(最好的?):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
这是你要找的吗?
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)
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 在他的第一个解决方案中提到的那样进行手动转换。
使用 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
插件为您的数据类提供默认的无参数构造函数
你真的想要一个单独的课程吗?您可以将属性添加到原始数据类:
data class PersonForm(
val firstName: String,
val lastName: String,
val age: Int,
val tel: String
) {
val name = "${firstName} ${lastName}"
}
这使用 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..
您可以使用 ModelMapper 映射到 Kotlin 数据类。关键是:
使用@JvmOverloads(生成不带参数的构造函数)
数据类成员的默认值
可变成员,var 代替 val 数据类 AppSyncEvent @JvmOverloads 构造函数(var field: String = "", var arguments: Map
对于 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"
}
不定期副业成功案例分享
@KotlinBuilder
的 MapStruct 是一种美观且快速的解决方案。查看其他答案(我在其中添加了@KotlinBuilder
信息)。