Подтвердить что ты не робот

Лучший способ сопоставить объекты данных Kotlin с объектами данных

Я хочу преобразовать/отобразить некоторые объекты класса данных в похожие объекты класса данных. Например, классы для веб-формы для классов для записей базы данных.

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?

Update: ModelMapper автоматически отображает поля с таким же именем (например, tel → tel) без объявлений сопоставления. Я хочу сделать это с классом данных Kotlin.

Update: Назначение каждого класса зависит от того, какое приложение, но возможно, оно размещено на другом уровне приложения.

Например:

  • данные из базы данных (объект базы данных) для данных для формы HTML (модель/модель представления)
  • Результат REST API для данных для базы данных

Эти классы схожи, но не совпадают.

Я хочу избежать обычных вызовов функций по следующим причинам:

  • Это зависит от порядка аргументов. Функция для класса со многими полями одного типа (например, String) будет легко разбита.
  • Многие объявления являются необязательными, хотя большинство сопоставлений можно разрешить с помощью соглашения об именах.

Конечно, библиотека, имеющая аналогичную функцию, предназначена, но информация о функции Kotlin также приветствуется (например, распространение в ECMAScript).

4b9b3361

Ответ 1

  • Проще всего (лучше?):

    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([email protected])
            }
        })
    }
    
  • Кэшированное отражение (хорошо работает, но не так быстро, как # 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)
    
  • Сохранение свойств на карте

    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

Это вы ищете?

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)

Ответ 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

Вам действительно нужен отдельный класс? Вы можете добавить свойства к исходному классу данных:

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

Ответ 5

Это работает с помощью Gson:

inline fun <reified T : Any> Any.mapTo(): T =
    GsonBuilder().create().run {
        toJson([email protected]).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

Вы можете использовать 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)
    

Ответ 7

Для ModelMapper вы можете использовать плагин компилятора без аргументов KaTlin, с помощью которого вы можете создать аннотацию, помечающую ваш класс данных, чтобы получить синтетический конструктор без аргументов для библиотек, использующих отражение. Ваш класс данных должен использовать 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"
}

Ответ 8

Использование 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)
}

Usage

Usage
val form = PersonForm(/** ... **/)
val record: PersonRecord = Mapper.convert(form)

Вам могут потребоваться некоторые правила отображения, если имена полей различаются. Смотрите начало работы
PS: используйте плагин kotlin no-args, чтобы иметь конструктор по умолчанию без аргументов с вашими классами данных