I have a Client api. The json response looks like this:
{
"clientId": 1,
"createdAt": null,
"updatedAt": null,
"monthlyPaymentAmount": null,
"person": {
// Omitted data here
},
"paymentType": {
// Omitted data here
},
"deliveryInstructions": null,
"referralName": null,
"referralPhoneNumber": null,
"status": 0,
"startDate": null,
"eventDate": null,
}
So, using the Kotlin data class file from JSON to automatically create data classes from the json response, I've got with the following Client data class which I've turned into a Room #Entity with ForeignKeys:
#Entity(
tableName = "client",
foreignKeys = [
ForeignKey(
entity = Account::class,
parentColumns = arrayOf("account_id"),
childColumns = arrayOf("account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Person::class,
parentColumns = arrayOf("person_id", "account_id"),
childColumns = arrayOf("person_id", "account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = PaymentType::class,
parentColumns = arrayOf("payment_type_id", "account_id"),
childColumns = arrayOf("payment_type_id", "account_id"),
),
],
indices = [
Index(value = arrayOf("client_id", "account_id"), unique = true)
]
)
data class Client(
#PrimaryKey
#ColumnInfo(name = "client_id") val clientId: Int,
#ColumnInfo(name = "delivery_notes") val deliveryInstructions: String,
#ColumnInfo(name = "event_date") val eventDate: Date,
#ColumnInfo(name = "monthly_payment_amount") val monthlyPaymentAmount: Float,
#ColumnInfo(name = "payment_type_id") val paymentType: Int,
#ColumnInfo(name = "person_id") val person: Int,
#ColumnInfo(name = "referral_name") val referralName: String,
#ColumnInfo(name = "start_date") val startDate: Date,
#ColumnInfo(name = "status") val status: Int,
#ColumnInfo(name = "updated_at") val updatedAt: Date,
#ColumnInfo(name = "synced_at") val syncedAt: Date,
)
There's also PaymentType and Person data classes which I'm omitting, but they do are Room #Entity's as well.
The Room database needs to match the following database structure that has this CREATE TABLE SQL statement:
CREATE TABLE client
(
client_id INTEGER NOT NULL,
account_id INTEGER NOT NULL,
updated_at TEXT NOT NULL,
synced_at TEXT NOT NULL,
person_id INTEGER NOT NULL,
payment_type_id INTEGER,
referral_name TEXT,
delivery_notes TEXT,
status INTEGER DEFAULT 1 NOT NULL,
monthly_payment_amount REAL,
start_date TEXT,
event_date TEXT,
CONSTRAINT client_fk1 FOREIGN KEY (account_id) REFERENCES account (account_id) ON DELETE CASCADE,
CONSTRAINT client_fk2 FOREIGN KEY (person_id, account_id) REFERENCES person (person_id, account_id) ON DELETE CASCADE,
CONSTRAINT client_fk4 FOREIGN KEY (payment_type_id, account_id) REFERENCES payment_type (payment_type_id, account_id),
CONSTRAINT client_pk PRIMARY KEY (client_id, account_id)
);
So, I've a Converters class to deserialize the json response into Client class as follows:
class Converters {
#TypeConverter
fun clientToJson(value: Client?): String? = Gson().toJson(value)
#TypeConverter
fun jsonToClient(value: String): Client = Gson().fromJson(value, Client::class.java)
#TypeConverter
fun paymentTypeToJson(value: PaymentType?): String? = Gson().toJson(value)
#TypeConverter
fun jsonToPaymentType(value: String): PaymentType =
Gson().fromJson(value, PaymentType::class.java)
#TypeConverter
fun objToJsonPerson(value: Person?): String? = Gson().toJson(value)
#TypeConverter
fun jsonToObjPerson(value: String): Person = Gson().fromJson(value, Person::class.java)
// Omitted list of converters here
}
I'm hesitant if the client converter above does correctly creates PaymentType and Person objects automatically (mostly convinced that no). That's why I would like to know what's the proper way to deserialize a json response with nested objects into Room entities with Foreign Keys?
I'm most confused with Foreing Keys though. What will happen when the converter above tries to parse the "person": {} object into the #ColumnInfo(name = "person_id") which is of type Int? Will it know that it is a ForeignKey and will create a Person::class automatically? How's the best/proper way to deserialize nested objects ensuring this relation between the tables is properly done?
Demonstration following on from previous answer
This a demonstration of inserting a single client based upon some slightly modified entities.
Account :-
#Entity
data class Account(
#PrimaryKey
val account_id: Long? = null,
val accountName: String
)
PaymentType
#Entity( primaryKeys = ["payment_type_id","account_id"])
data class PaymentType(
#ColumnInfo(name = "payment_type_id")
val paymentTypeId: Long,
#ColumnInfo(name = "account_id")
val accountId: Long,
val paymentTypeName: String
)
added accountId (account_id column) to suit Foreign Key constraints in the ClientTable (as per question)
composite primary key
Person (likewise)
#Entity( primaryKeys = ["person_id","account_id"])
data class Person(
#ColumnInfo(name = "person_id")
val personId: Long,
#ColumnInfo(name = "account_id")
val accountId: Long,
val personName: String
)
Client as suggested
data class Client(
val clientId: Long,
val deliveryInstructions: String,
val eventDate: Date,
val monthlyPaymentAmount: Float,
val referralName: String,
val startDate: Date,
val status: Long,
val updatedAt: Date,
val syncedAt: Date,
val person: Person,
val paymentType: PaymentType
) {
fun getClientAsClientTable(): ClientTable {
return ClientTable(
this.clientId,
this.deliveryInstructions,
this.eventDate,
this.monthlyPaymentAmount,
this.paymentType.paymentTypeId,
this.person.personId,
this.referralName,
this.startDate,
this.status,
this.updatedAt,
this.syncedAt
)
}
}
ideally id's should be Long rather than Int as they have the potential to overflow an Int. So Long's have been used.
ClientTable formally (Client) :-
#Entity(
tableName = "client",
foreignKeys = [
ForeignKey(
entity = Account::class,
parentColumns = arrayOf("account_id"),
childColumns = arrayOf("account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Person::class,
parentColumns = arrayOf("person_id", "account_id"),
childColumns = arrayOf("person_id", "account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = PaymentType::class,
parentColumns = arrayOf("payment_type_id", "account_id"),
childColumns = arrayOf("payment_type_id", "account_id"),
),
],
indices = [
Index(value = arrayOf("client_id", "account_id"), unique = true)
]
)
data class ClientTable(
#PrimaryKey
#ColumnInfo(name = "client_id") val clientId: Long,
#ColumnInfo(name = "delivery_notes") val deliveryInstructions: String,
#ColumnInfo(name = "event_date") val eventDate: Date,
#ColumnInfo(name = "monthly_payment_amount") val monthlyPaymentAmount: Float,
#ColumnInfo(name = "payment_type_id") val paymentTypeid: Long,
#ColumnInfo(name = "person_id") val personid: Long,
#ColumnInfo(name = "referral_name") val referralName: String,
#ColumnInfo(name = "start_date") val startDate: Date,
#ColumnInfo(name = "status") val status: Long,
#ColumnInfo(name = "updated_at") val updatedAt: Date,
#ColumnInfo(name = "synced_at") val syncedAt: Date,
#ColumnInfo(name = "account_id") var accountId: Long = 1 //????? ADDED
)
NOTE the addition of the accountId
Converters
class Converters {
#TypeConverter
fun dateToLong(date: Date): Long {
return date.time / 1000 // divided by 1000 to strip milliseconds as easier to handle dates
}
#TypeConverter
fun dateFromLong(dateAsLong: Long): Date {
return Date(dateAsLong * 1000) // reapply milliseconds
}
}
AllDao (as it implies all of them together) :-
#Dao
abstract class AllDao {
#Insert(onConflict = IGNORE)
abstract fun insert(account: Account): Long
#Insert(onConflict = IGNORE)
abstract fun insert(paymentType: PaymentType): Long
#Insert(onConflict = IGNORE)
abstract fun insert(person: Person): Long
#Insert(onConflict = IGNORE)
abstract fun insert(clientTable: ClientTable): Long
#Query("SELECT count(*) >= 1 FROM account WHERE account_id=:accountId")
abstract fun doesAccountExistByAccountId(accountId: Long): Boolean
#Query("SELECT count(*) >= 1 FROM paymenttype WHERE account_id=:accountId AND payment_type_id=:paymentTypeId")
abstract fun doesPaymentTypeExistByAccountIdPaymentTypeId(accountId: Long, paymentTypeId: Long): Boolean
#Query("SELECT count(*) >= 1 FROM person WHERE account_id=:accountId AND person_id=:personId")
abstract fun doesPersonExistByAccountIdPersonId(accountId: Long, personId: Long): Boolean
#Query("")
#Transaction
fun insertFromAPIJson(json: String): Long {
var rv: Long = -1
val client = Gson().fromJson(json,Client::class.java)
val clientTable = client.getClientAsClientTable()
insert(Account(client.person.accountId,"NO NAME"))
val accountExits = doesAccountExistByAccountId(client.person.accountId)
insert(PaymentType(client.paymentType.paymentTypeId,client.paymentType.accountId,client.paymentType.paymentTypeName))
val paymentTypeExists = doesPaymentTypeExistByAccountIdPaymentTypeId(client.paymentType.accountId,client.paymentType.paymentTypeId)
insert(Person(client.person.personId, client.person.accountId, client.person.personName))
val personExists = doesPersonExistByAccountIdPersonId(client.person.accountId,client.person.personId)
if (accountExits && paymentTypeExists && personExists) {
clientTable.accountId = client.person.accountId
rv = insert(clientTable)
}
return rv
}
}
Obviously note the insertFromAPIJson function
Also note abstract class rather than an interface
Note the improvised account name (something you will have to determine how to name)
TheDatabase the abstract class annotated with #Database including a basic getInstance function :-
#TypeConverters(Converters::class)
#Database(entities = [Account::class,ClientTable::class,PaymentType::class,Person::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
Finally adding the single client from the JSON (again the client is built and the JSON extracted to mimic the API). MainActivity :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
/* Create a Client ready to be converted to JSON */
val clientx = Client(
clientId = 1,
deliveryInstructions = "x",
eventDate = Date(),
monthlyPaymentAmount = 111.11F, referralName = "Fred", startDate = Date(), status = 1, updatedAt = Date(), syncedAt = Date(),
Person(10,1,"Bert"), paymentType = PaymentType(20,1,"Credit Card"))
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
dao.insertFromAPIJson(Gson().toJson(clientx))
}
}
The Result
Using App Inspection :-
The Account has been added :-
The PaymentType has been added :-
The Person has been added :-
and the Client :-
Run again
As expected the data remains the same due to onConflict IGNORE
Will it know that it is a ForeignKey and will create a Person::class automatically?
Absolutely not.
#ForeignKey defines a ForeignKey constraint i.e. a rule that says that the column(s) to which the constraint is applied must be a value in the referenced column(s) in the referenced table. The referenced (Parent) tables have to exist and be populated accordingly with values that match the rule.
Type converters are used to convert an unhandled type (not a type this is integer (e.g. Int Long Byte etc), String, decimal number (Float, Double etc) or ByteArray) into a handled type.
As an example your :-
#TypeConverter
fun clientToJson(value: Client?): String? = Gson().toJson(value)
would convert a single column such as
client: Client
from a Client To a JSON String and store that string in the client column. It is not going to split the client into individual values and place them into individual columns.
So with your retrieved JSON String you extract Client objects with embedded Person and Payment Type.
You can only successfully insert the Client if all the Foreign Keys can be met.
So you should probably check that the account_id exists in the account table.
The check that the person_id,account_id exists in the person_table and so on before inserting the Client otherwise the insert will fail.
If the checks fail to identify the rows then you either have to abort or insert the appropriate rows into the tables.
Assuming that your source data is referentially correct. Then you should first extract the highest level parent (Account I believe) inserting them. You can then extract the next level (Person and Payment Type) and insert them and then finally insert the Client. This way the Foreign Keys should exist.
Al alternative would be to turn off Foreign key support and load the data and then turn on Foreign Key support back on. However, if the data is not referentially correct you may encounter Foreign Key constraint conflicts.
e.g. db.openHelper.writableDatabase.setForeignKeyConstraintsEnabled(false)
I've got with the following Client data class which I've turned into a Room #Entity with ForeignKeys:
based upon the JSON you will have issues with the null values unless you ensure that the values are changed to appropriate values.
e.g. "updatedAt": null, associated with #ColumnInfo(name = "updated_at") val updatedAt: Date, unless the TypeConverter returns a non-null value will fail.
The Room database needs to match the following database structure that has this CREATE TABLE SQL statement:
It does not e.g. you have:-
payment_type_id INTEGER, but #ColumnInfo(name = "payment_type_id") val paymentType: Int, The former does not have the NOT NULL constraint, the latter has an implicit NOT NULL (val paymentType: Int? does not have the implicit NOT NULL)
repeated for a number of columns
status INTEGER DEFAULT 1 NOT NULL, but #ColumnInfo(name = "status") val status: Int, the latter does not have the default value using defaultValue = "1" in the #ColumnInfo annotation would apply it.
However, you cannot use the convenience #Insert annotated function as it will ALWAYS supply a value. To have the default value apply you would have to use #Query("INSERT INTO (csv_of_the_columns_that_are_not_to_have_a_default_value_applied) VALUES ....
CONSTRAINT client_pk PRIMARY KEY (client_id, account_id) but #PrimaryKey #ColumnInfo(name = "client_id") val clientId: Int,. Only the client ID is the Primary Key. You do have indices = [Index(value = arrayOf("client_id", "account_id"), unique = true)]. However, you should instead have primaryKeys = arrayOf("client_id", "account_id")
Additional
Having had a closer look. I believe that your issue is not with type converters nor at present with Foreign keys but with what is an attempt to fit the square peg into the round hole.
Without delving into trying to ignore fields from a JSON perspective a solution to what I believe is the core issue is that the you cannot just fit the Client with Person and Payment objects Embedded into the Client that you want to store.
So first consider this alternative class for the Entity renamed ClientTable :-
#Entity(
tableName = "client",
foreignKeys = [
ForeignKey(
entity = Account::class,
parentColumns = arrayOf("account_id"),
childColumns = arrayOf("account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Person::class,
parentColumns = arrayOf("person_id", "account_id"),
childColumns = arrayOf("person_id", "account_id"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = PaymentType::class,
parentColumns = arrayOf("payment_type_id", "account_id"),
childColumns = arrayOf("payment_type_id", "account_id"),
),
],
indices = [
Index(value = arrayOf("client_id", "account_id"), unique = true)
]
)
data class ClientTable(
#PrimaryKey
#ColumnInfo(name = "client_id") val clientId: Int,
#ColumnInfo(name = "delivery_notes") val deliveryInstructions: String,
#ColumnInfo(name = "event_date") val eventDate: Date,
#ColumnInfo(name = "monthly_payment_amount") val monthlyPaymentAmount: Float,
#ColumnInfo(name = "payment_type_id") val paymentTypeid: Int,
#ColumnInfo(name = "person_id") val personid: Int,
#ColumnInfo(name = "referral_name") val referralName: String,
#ColumnInfo(name = "start_date") val startDate: Date,
#ColumnInfo(name = "status") val status: Int,
#ColumnInfo(name = "updated_at") val updatedAt: Date,
#ColumnInfo(name = "synced_at") val syncedAt: Date,
/* Not required but may be useful BUT will not be columns in the table */
#Ignore
val person: Person,
#Ignore
val paymentType: PaymentType
)
The only changes are the two additional BUT #Ignore annotated vals, for the Person and for the PaymentType. The #Ignore results in them not being included as a column in the table. They are there just for demonstration (you might have problems with them being null when extracting the data from the database).
Note that for testing the PaymentType is :-
#Entity
data class PaymentType(
#PrimaryKey
val paymentTypeId: Long? = null,
val paymentTypeName: String
)
and the Person is :-
#Entity
data class Person(
#PrimaryKey
val personId: Long,
val personName: String
)
so the // Omitted data here does not cause issues.
Instead of your JSON the following JSON has been used (however it is built on the fly) :-
{"clientId":1,"deliveryInstructions":"x","eventDate":"Jan 21, 2022 10:57:59 AM","monthlyPaymentAmount":111.11,"paymentType":{"paymentTypeId":20,"paymentTypeName":"Credit Card"},"person":{"personId":10,"personName":"Bert"},"referralName":"Fred","startDate":"Jan 21, 2022 10:57:59 AM","status":1,"syncedAt":"Jan 21, 2022 10:57:59 AM","updatedAt":"Jan 21, 2022 10:57:59 AM"}
A simple not Type Converter json extractor to mimic the API has been added:-
class JsonApiExample {
fun testExtractJsonFromString(json: String): Client {
return Gson().fromJson(json,Client::class.java)
}
}
Now to the other peg The Client with the embedded Person/PaymentType and WITHOUT the personId and paymentTypeId that are not fields in the JSON:-
data class Client(
val clientId: Int,
val deliveryInstructions: String,
val eventDate: Date,
val monthlyPaymentAmount: Float,
val referralName: String,
val startDate: Date,
val status: Int,
val updatedAt: Date,
val syncedAt: Date,
val person: Person,
val paymentType: PaymentType
) {
fun getClientAsClientTable(): ClientTable {
return ClientTable(
this.clientId,
this.deliveryInstructions,
this.eventDate,
this.monthlyPaymentAmount,
this.paymentType.paymentTypeId!!.toInt(),
this.person.personId.toInt(),
this.referralName,
this.startDate,
this.status,
this.updatedAt,
this.syncedAt,
this.person,
this.paymentType
)
}
}
As you can see the important bit is the getClientAsClientTable funtion. This will generate and return a ClientTable Object and effectively make the square peg round to fit.
So testing it, as far as creating a ClientTable that could be inserted (Foreign Keys permitting, which would not be the case due to there being no account_id column in the ClientTable nora field in the Client class) consider :-
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
/* Create a Client ready to be converted to JSON */
val clientx = Client(
clientId = 1,
deliveryInstructions = "x",
eventDate = Date(),
monthlyPaymentAmount = 111.11F, referralName = "Fred", startDate = Date(), status = 1, updatedAt = Date(), syncedAt = Date(),
Person(10,"Bert"), paymentType = PaymentType(20,"Credit Card"))
/* Convert the Client to JSON mimicing the API and write it to the log to allow inspection */
val jsonClientX = Gson().toJson(clientx)
Log.d("TESTIT",jsonClientX)
/* Extract the Client from the JSON */
val clientxrevisited = JsonApiExample().testExtractJsonFromString(jsonClientX)
/* Extract the ClientTable from the Client */
val ClientTable = clientxrevisited.getClientAsClientTable()
/* Allow a Break point to be placed so the results can be inspected*/
if (1 == 1) {
Log.d("TESTIT","Testing")
}
}
}
When run with a BreakPoint :-
The ticks are the columns
The highlighted are the embedded objects from which you would be able to insert or ignore into the respective tables.
note that the fields may well be different just id and name were chosen just to demonstrate.
So to RECAP two classes; one for the JSON extract/import, and the other for the Entity/Table with a means of turning one into the other.
A single class may be possible if you can ascertain how to ignore/map JSON fields e.g. perhaps this How to add and ignore a field for json response
I created Alarm class and annotate it as Entity on Android Studio.
In this class I have put id variable as Entity Primary Key and it is auto generate.
#Entity(tableName = "alarm_data_table")
class Alarm(
#ColumnInfo(name = "alarm_hour")
var alarmHour: Int,
#ColumnInfo(name = "alarm_min")
var alarmMin: Int,
#ColumnInfo(name = "is_am")
var isAM: Boolean,
var days: ArrayList<Int>?,
#ColumnInfo(name = "is_on")
var isOn: Boolean,
var label: String,
#PrimaryKey(autoGenerate = true)
val id: Int = 0
) {
fun setAlarmSchOnOff(isOn: Boolean, activity: Activity){
if (isOn){setAlarmScheduleOn(activity)}
if (!isOn){setAlarmScheduleOff(activity)}
}
fun setAlarmScheduleOn (activity: Activity) {
val alarmManager = activity.getSystemService(Context.ALARM_SERVICE) as AlarmManager
Log.d("ALARM ON", Integer.toString(id))
val alarmIntent = Intent(activity, AlarmReceiver::class.java).let { intent ->
intent.putExtra(Constants().ALARM_LABEL_KEY,label)
PendingIntent.getBroadcast(
activity,
id,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
val calendar: Calendar = Calendar.getInstance().apply {
timeInMillis = System.currentTimeMillis()
set(Calendar.HOUR_OF_DAY, alarmHour)
set(Calendar.MINUTE, alarmMin)
}
days?.let {
it.forEach {
calendar.set(Calendar.DAY_OF_WEEK, it) }
alarmManager.setInexactRepeating(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
AlarmManager.INTERVAL_HOUR,
alarmIntent
)
} ?:run{
alarmManager.set(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
alarmIntent)
}
}
fun setAlarmScheduleOff(activity: Activity) {
val alarmManager = activity.getSystemService(Context.ALARM_SERVICE) as AlarmManager
Log.d("ALARM OFF", Integer.toString(alarmId))
val alarmIntent = Intent(activity, AlarmReceiver::class.java).let { intent ->
PendingIntent.getBroadcast(
activity,
id,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)}
alarmManager.cancel(alarmIntent)
}
}
The problem is every time I tried to retrieve id for setAlarm method it will always return 0.
2019-10-30 15:38:07.441 11066-11066/com.andreasgift.myclock D/ALARM ON: 0
Is there any way to return the id value after Entity update the value/ successfully insert it into table.
Is there any way to return the id value after Entity update the value/
successfully insert it into table
Change to use (optional)
:-
#PrimaryKey(autoGenerate = true)
var id: Long = 0 /*<<<<<<<<<< var not val + Long instead of Int */
you should really use Long for id columns as
a) the SQliteDatabase insert method that is ultimately used, returns a long and
b) (which is why a is as it is)) a rowid (which is effectively what autogenerate uses ) can be a value larger then an Int can hold (i.e. a 64 bit signed integer).
If you wanted to use Int for id then you could use myAlarm.id = (alarmDao.insertAlarm(myAlarm)).toInt() instead of myAlarm.id = alarmDao.insertAlarm(myAlarm)
And use a Dao
#Insert
fun insertAlarm(alarm :Alarm) :Long
i.e. set it so that it returns :Long
The insert now returns the id (or -1 if the insert didn't insert the row). If you still use the Alarm object after the insert then you could use something like :-
myAlarm.id = alarmDao.insertAlarm(myAlarm) /*<<<<<<<<<< SET THE id according to the inserted id */
Given an update request for a record in DB, I have to find a difference between the payload and existing data in DB then create a new Object which has updated fields with Payload values and rest as Null.
I have created a function which gives me a list of field names which were updated, But I'm unable to create a new object which has values for only these updated fields.The problem is that the function uses "field: Field in cpayload.javaClass.declaredFields" which is kind of generic so I'm unable to set these fields.
fun findupdatedFieldsList(cpayload: Customer, cEntity: Customer): List<String> {
// var customerToPublish = Customer()
val updatedFieldsList: MutableList<String>
updatedFieldsList = ArrayList()
for (field: Field in cpayload.javaClass.declaredFields) {
field.isAccessible = true
val value1 = field.get(cpayload).toString()
val value2 = field.get(cEntity).toString()
!Objects.equals(value1, value2).apply {
if (this) {
// customerToPublish.birthDate=field.get(cpayload).toString()
updatedFieldsList.add(field.name)
}
}
}
return updatedFieldsList
}
#Entity
#Table
data class Customer(
#Id
val partyKey: UUID,
var preferredName: String?,
var givenName: String?,
var lastName: String?,
var middleName: String?,
var emailAddress: String,
var mobileNumber: String,
val birthDate: String?,
val loginOnRegister: Boolean,
var gender: Gender?,
var placeOfBirth: String?,
var createdDate: LocalDateTime = LocalDateTime.now(),
var updatedDate: LocalDateTime = LocalDateTime.now()
)
Desired Output
val customer = Customer(
preferredName = Updated name,
partyKey = partyKey.value,
givenName = Updated name,
lastName = null,
middleName = null,
emailAddress = Updated email,
mobileNumber = null,
birthDate = null,
gender = null,
placeOfBirth = null
)
I was able to construct a solution using Kotlin's reflect. It is generic and can be applied to any Kotlin class that have primary constructor. Unfortunately it won't work with Java classes
You would need to add kotlin-reflect package to your build tool config, e.g. for Gradle:
implementation 'org.jetbrains.kotlin:kotlin-reflect:XXXXXX'
First we will build a function to extract updated properties. Please take a note that we also need to extract properties that are mandatory (non-nullable and without default). We add them to a map of propertyName -> propertyValue:
fun Map<String?, KParameter>.isOptional(name: String) = this[name]?.isOptional ?: false
fun <T : Any> findUpdatedProperties(payload: T, entity: T): Map<String, Any?> {
val ctorParams = payload::class.primaryConstructor!!.parameters.associateBy { it.name }
return payload::class.memberProperties.map { property ->
val payloadValue = property.call(payload)
val entityValue = property.call(entity)
if (!Objects.equals(payloadValue, entityValue) || (!ctorParams.isOptional(property.name))) {
property.name to payloadValue
} else {
null
}
}
.filterNotNull()
.toMap()
}
Then we call this function and construct a new instance of provided class:
fun <T : Any> constructCustomerDiff(clazz: KClass<T>, payload: T, entity: T): T {
val ctor = clazz.primaryConstructor!!
val params = ctor.parameters
val updatedProperties = findUpdatedProperties(payload, entity)
val values = params.map { it to updatedProperties[it.name] }.toMap()
return ctor.callBy(values)
}
Take a note that missing primary constructor will throw NullPointerException because of use of !!.
We could call this funcion as constructCustomerDiff(Customer::class, payload, entity), but we can do better with reified types:
inline fun <reified T : Any> constructCustomerDiff(payload: T, entity: T): T {
return constructCustomerDiff(T::class, payload, entity)
}
Now we can use this function in convenient Kotlin style:
val id = UUID.randomUUID()
val payload = Customer(
partyKey = id,
preferredName = "newName",
givenName = "givenName"
)
val entity = Customer(
partyKey = id,
preferredName = "oldName",
givenName = "givenName" // this is the same as in payload
)
val x = constructCustomerDiff(payload, entity)
assert(x.partyKey == id && x.givenName == null || x.preferredName == "newName")