Home > Back-end >  Copy most attributes from one class object to another class
Copy most attributes from one class object to another class

Time:09-15

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.

  • Related