Кращий спосіб зіставити об'єкти даних 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.