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

时间:2016-08-29 06:01:02

标签: java kotlin modelmapper

我想转换/映射一些"数据"类对象类似"数据"类对象。例如,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"的功能。手动上课。 (参见ModelMapper示例:https://github.com/jhalterman/modelmapper/blob/master/examples/src/main/java/org/modelmapper/gettingstarted/GettingStartedExample.java

如何映射此类"数据"科特林的物品?

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

更新 每个类的目的取决于应用程序的类型,但这些应用程序可能放在应用程序的不同层中。

例如:

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

这些类很相似,但不一样。

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

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

当然,有一个具有类似功能的库,但也欢迎Kotlin功能的信息(如在ECMAScript中传播)。

8 个答案:

答案 0 :(得分:33)

  1. 最简单(最好?):

    td
  2. 反思(表现不佳):

    fun PersonForm.toPersonRecord() = PersonRecord(
            name = "$firstName $lastName",
            age = age,
            tel = tel
    )
    
  3. 缓存反射(性能良好但不如#1快):

    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)
            }
        })
    }
    
  4. Storing Properties in a Map

    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)
    

答案 1 :(得分:15)

这是你在寻找吗?

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)

答案 2 :(得分:3)

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

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

答案 3 :(得分:3)

使用MapStruct:

@Mapper
interface PersonConverter {

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

    @InheritInverseConfiguration
    fun convertToModel(personDto: PersonDto) : Person

}

使用:

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)

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

答案 4 :(得分:1)

使用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插件将默认no-arg构造函数与数据类一起使用

答案 5 :(得分:0)

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

,因此不允许空值的

为空

答案 6 :(得分:0)

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

  • 使用@JvmOverloads(生成不带参数的构造函数)
  • 数据类成员的默认值
  • 可变成员,用var代替val

    public class TimestampFinder {
        private static final long SOME_CONSTANT = 10_000;
        private List<Instant> timestamps = ... ; // initialize and sort
    
        public Instant find(Instant inputTs) {
            int index = Collections.binarySearch(timestamps, inputTs, (t, key) -> {
                if (t.isBefore(key) && key.isBefore(t.plusMillis(SOME_CONSTANT))) {
                    // inputTs is part of the duration after t
                    // return 0 to indicate that we've found a match
                    return 0;
                }
                // inputTs not in interval
                // use Instant's implementation of Comparator to indicate to binarySearch if it should continue the search in the upper or lower half of the list
                return t.compareTo(key);
            });
            return index >= 0 ? timestamps.get(index) : null;
        }
    } 
    

答案 7 :(得分:0)

对于ModelMapper,您可以使用Kotlin's no-arg compiler plugin,使用它可以创建标记您的数据类的注释,以获取使用反射的库的合成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"
}