Between two separate data classes, Person
and PersonRecord
, which share the same attribute names, I want an elegant way to copy the values from one class's attributes to the other's.
I have a data class, say for example Person
, that defines the business logic data of a person in the application.
import kotlinx.serialization.Serializable
data class Person(
val id: String,
val name: String,
val age: Int,
val currentEmployment: Employment,
val workPermit: WorkPermit
)
@Serializable
data class Employment(
val employer: String,
val job: String,
val yearsWithEmployer: Double
)
@Serializable
data class WorkPermit(
val nationality: String,
val visa: String
)
I need to use these with an AWS DynamoDB client, but this question doesn't really concern DynamoDB specifically. I'll explain my usage below.
For several reasons, I've decided to implement a DAO class that is essentially a copy of the class Person
, called PersonRecord
except the fields containing complex types, i.e., Employment
and WorkPermit
, are stored as Strings instead. Also, all the fields are mutable and nullable. I had to make it this way because it's supposed to be a mapper class for DynamoDB Enhanced Client (doc).
Annotating this class as @DynamoDbBean
defines how the client writes items into a specified table.
package util
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable
import software.amazon.awssdk.enhanced.dynamodb.Key
import software.amazon.awssdk.enhanced.dynamodb.TableSchema
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey
@DynamoDbBean
internal data class PersonRecord(
@get: DynamoDbPartitionKey
@get: DynamoDbSortKey
var id: String? = null,
var name: String? = null,
var age: Int? = null,
var currentEmployment: String? = null,
var workPermit: String? = null,
)
class PersonDao(
ddb: DynamoDbEnhancedClient,
personTableName: String
) {
private val personTable: DynamoDbTable<PersonRecord> = ddb.table(
personTableName,
TableSchema.fromBean(PersonRecord::class.java)
)
private fun toPersonRecord(person: Person): PersonRecord =
PersonRecord(
id = person.id,
name = person.name,
age = person.age,
currentEmployment = Json.encodeToString(person.currentEmployment),
workPermit = Json.encodeToString(person.workPermit)
)
private fun toPerson(personRecord: PersonRecord): Person =
Person(
id = personRecord.id!!,
name = personRecord.name!!,
age = personRecord.age!!,
currentEmployment = Json.decodeFromString(
personRecord.currentEmployment!!
),
workPermit = Json.decodeFromString(
personRecord.workPermit!!
)
)
fun writePerson(person: Person) =
personTable.putItem(toPersonRecord(person))
fun readPerson(id: String): Person? {
val personRecord = personTable.getItem(
Key.builder()
.partitionValue(id)
.build()
)
return if (personRecord != null) toPerson(personRecord)
else null
}
}
I am using the public functions readPerson
and writePerson
to read and write the pretty Person
class, while these functions internally convert to and fro PersonRecord
.
Is there a way to copy between the different classes Person
and PersonRecord
more elegantly? If, in the future, we change the shape of Person
slightly, there's a lot to change in the PersonRecord
and PersonDao
classes too. In particular, I need a way to handle decoding String
to Employment
and WorkPermit
, and vice-versa.
In the example above, it'd be trivial to add a field or two, but in my actual application I'm dealing with over a dozen fields, and a bunch of unit tests intricately involved with the fields themselves.
Someone suggested to use class reflections, but I don't understand how I'd use it based on what the Kotlin docs describe.
CodePudding user response:
You can try to read Person properties into a map via reflections (there is no other way) and use delegated properties feature to construct PersonRecord from that map.
https://kotlinlang.org/docs/delegated-properties.html#storing-properties-in-a-map
Here is a sample of reading via reflection https://stackoverflow.com/a/38688203/8642957
CodePudding user response:
Yes, MapStruct is great and it's available in kotlin via kapt.