Лучший способ сопоставить объекты данных 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).
Ответы
Ответ 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. Ключи:
Ответ 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
, чтобы иметь конструктор по умолчанию без аргументов с вашими классами данных