Кращий спосіб зіставити об'єкти даних Kotlin з об'єктами даних


75

Я хочу перетворити / зіставити деякі об'єкти класу "data" у подібні об'єкти класу "data". Наприклад, класи для веб-форми до класів для записів бази даних.

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, коли робимо ці класи / поля відкритими, але ми повинні реалізувати функції класу "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).


Будь ласка, опишіть, як ви хочете використовувати зіставлені класи. Яка мета створення двох окремих форматів даних?
— воддан

Ніколи не чув про дублювання моделі даних (крім випадків застарілого коду). Зазвичай даними, з якими ви працюєте (Модель перегляду), є дані, які ви вводите в базу даних.
— воддан

Одним із варіантів @voddan було б виставляти лише частини моделі домену різним споживачам API. Наявність окремого DTO для кожного виду доменної моделі набагато чистіше, ніж використання, наприклад, JsonView IMHO
— miensol

Вам справді потрібні ці класи, щоб бути класами даних?
— Олексій Андрєєв

Відповіді:


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інформацію).
— Арберг

3
Насправді я перестав використовувати MapStruct зараз і просто пішов із першим згаданим рішенням, тобто. ручне відображення. MapStruct працює на основі Java, тому не забезпечує нульової безпеки. Також я вважаю, що я в будь-якому випадку отримую безпеку під час компіляції за допомогою простого ініціалізатора kotlin, коли в конструкторі відсутні значення за замовчуванням. Отже, якщо в об’єкт даних додано поле, я отримую помилку компіляції, що я і хочу.
— Арберг

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). Я хочу написати лише спеціальні правила, такі як ім'я + прізвище => ім'я.
— 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 для уникнення проблеми з конструктором ():

GitHub: mapstruct-kotlin Позо

Анотуйте класи даних за допомогою @KotlinBuilder. Це створить PersonBuilderклас, який MapStruct використовує, таким чином ми уникаємо руйнування інтерфейсу класу даних за допомогою конструктора ().

@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, яке дозволяє використовувати підхід будівельника, який не вимагає конструктора (): це (нульове, нульове) фіаско
— FearlessHyena

4

Ви справді хочете для цього окремий клас? Ви можете додати властивості до вихідного класу даних:

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

1
Заняття спікератом не обов’язково. Але я хочу уникати залежно від порядку аргументів.
— sunnyone

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

із значеннями, що не допускають нульового значення, дозволеним бути нульовим, оскільки Gson використовує sun.misc.Unsafe ..


1
Гсон досить млявий
— 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 ви можете використовувати плагін компілятора no-arg від Kotlin , за допомогою якого ви можете створити анотацію, яка позначає ваш клас даних, щоб отримати синтетичний конструктор 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"
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.