Room - How to set nullable Foreign Key - kotlin

In my Kotlin Android codebase I have the following 2 entities..
#Entity(
tableName = ModuleConfiguration.tableName,
primaryKeys = [ModuleConfiguration.COL_ID],
foreignKeys = arrayOf(
ForeignKey(
entity = Module::class,
parentColumns = [Module.COL_ID],
childColumns = [ModuleConfiguration.COL_MODULE_ID],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Group::class,
parentColumns = [Group.COL_ID],
childColumns = [ModuleConfiguration.COL_GROUP_ID]
)
)
)
class ModuleConfiguration(
#ColumnInfo(name = COL_ID)
var id: String = UUID.randomUUID().toString(),
#ColumnInfo(name = COL_TABLE)
var table: String,
#ColumnInfo(name = COL_FIELD_NAME)
var fieldName: String,
#ColumnInfo(name = COL_FIELD_LABEL)
var fieldLabel: String,
#ColumnInfo(name = COL_FIELD_TYPE)
var fieldType: ModuleConfigurationItemType,
#ColumnInfo(name = COL_GROUP_ID)
var groupId: String?,
#ColumnInfo(name = COL_MODULE_ID)
var moduleId: String,
#ColumnInfo(name = COL_POSITION)
var position: Int,
#ColumnInfo(name = COL_VISIBLE)
var visible: Int = 1, //Usually visible
#ColumnInfo(name = COL_READ_ONLY)
var readOnly: Int = 0, //Usually false
#ColumnInfo(name = COL_REQUIRED)
var required: Int = 0, //Usually false
#ColumnInfo(name = COL_CREATED_AT)
var createdAt: Long = CustomDateTimeUtil.getTodayInUTC(),
#ColumnInfo(name = COL_UPDATED_AT)
var updatedAt: Long = CustomDateTimeUtil.getTodayInUTC()
) : Cloneable, Serializable, Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readString() ?: "",
parcel.readString() ?: "",
parcel.readString() ?: "",
fieldType = ModuleConfigurationItemType.valueOf(parcel.readString() ?: FieldType.UNKNOWN.name),
groupId = parcel.readString(),
moduleId = parcel.readString() ?: "",
position = parcel.readInt(),
visible = parcel.readInt(),
readOnly = parcel.readInt(),
required = parcel.readInt(),
createdAt = parcel.readLong(),
updatedAt = parcel.readLong()
) {
}
fun getViewType() : ModuleConfigurationItemType {
return this.fieldType
}
override fun equals(other: Any?): Boolean {
return super.equals(other)
}
override fun hashCode(): Int {
return super.hashCode()
}
override fun clone(): Any {
return super.clone()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(id)
parcel.writeString(table)
parcel.writeString(fieldName)
parcel.writeString(fieldLabel)
parcel.writeString(fieldType.name)
parcel.writeString(groupId)
parcel.writeString(moduleId)
parcel.writeInt(position)
parcel.writeInt(visible)
parcel.writeInt(readOnly)
parcel.writeInt(required)
parcel.writeLong(createdAt)
parcel.writeLong(updatedAt)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ModuleConfiguration> {
const val tableName = "module_configuration"
const val COL_ID = "id"
const val COL_MODULE_ID = "module_id"
const val COL_TABLE = "table"
const val COL_FIELD_NAME = "field_name"
const val COL_FIELD_LABEL = "field_label"
const val COL_FIELD_TYPE = "field_type"
const val COL_GROUP_ID = "group_id"
const val COL_VISIBLE = "visible"
const val COL_READ_ONLY = "read_only"
const val COL_REQUIRED = "required"
const val COL_POSITION = "position"
const val COL_CREATED_AT = "created_at"
const val COL_UPDATED_AT = "updated_at"
const val COL_CLIENT_ID = "client_id"
override fun createFromParcel(parcel: Parcel): ModuleConfiguration {
return ModuleConfiguration(parcel)
}
override fun newArray(size: Int): Array<ModuleConfiguration?> {
return arrayOfNulls(size)
}
}
}
and Group Entity
#Entity(
tableName = Group.tableName,
primaryKeys = [Group.COL_ID]
)
class Group(
#ColumnInfo(name = COL_ID)
var id: String = UUID.randomUUID().toString(),
#ColumnInfo(name = COL_NAME)
var name: String,
#ColumnInfo(name = COL_CREATED_AT)
var createdAt: Long = CustomDateTimeUtil.getTodayInUTC(),
#ColumnInfo(name = COL_UPDATED_AT)
var updatedAt: Long = CustomDateTimeUtil.getTodayInUTC()
) : Cloneable, Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readString() ?: "",
parcel.readLong(),
parcel.readLong()
) {
}
override fun equals(other: Any?): Boolean {
return super.equals(other)
}
override fun hashCode(): Int {
return super.hashCode()
}
override fun clone(): Any {
return super.clone()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(id)
parcel.writeString(name)
parcel.writeLong(createdAt)
parcel.writeLong(updatedAt)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Group> {
const val tableName = "group"
const val COL_ID = "id"
const val COL_NAME = "name"
const val COL_CREATED_AT = "created_at"
const val COL_UPDATED_AT = "updated_at"
const val COL_CLIENT_ID = "client_id"
override fun createFromParcel(parcel: Parcel): Group {
return Group(parcel)
}
override fun newArray(size: Int): Array<Group?> {
return arrayOfNulls(size)
}
}
}
My problem is that I am trying to set nullable foreign key, as you can see in entity named ModuleConfiguration, there is column named group_id, which is nullable.
#ColumnInfo(name = COL_GROUP_ID)
var groupId: String?,
Since, this column belongs to other entity named Group, I am trying to make it foreign key, but when I do that, I am getting following error
SQLiteConstraintException: FOREIGN KEY constraint failed (code 787)
On searching online, I found following answers on stackoverflow:
Android with Room - How to set a foreign key nullable
or
Nullable foreign key on room
but this did not help me as these answer suggest to change primitive types like int or long into non-primitive types like Integer or Long for allowing them to be null. I am already using String as type.
any help would be highly appreciated.
Thanks in advance.

Related

kotlin exposed select with foreign key

I have two tables like as I described below
object Categories : LongIdTable() {
val name = varchar(name = "name", length = 50).uniqueIndex()
val count = integer(name = "count")
val page = integer(name = "page")
}
object Images : LongIdTable() {
val color = varchar(name = "color", length = 100)
val blurHash = varchar(name = "blurHash", length = 200)
val unsplashId = varchar(name = "unsplashId", length = 200)
val category = reference(name = "category", Categories.name).nullable()
val isAvailable = bool(name = "isAvailable")
val imagePath = varchar(name = "imagePath", length = 500)
}
and here is my Image & Category data classes
#Serializable
data class Image(
val id: Long,
val unsplashId: String,
val isAvailable: Boolean = false,
val color: String,
val blurHash: String,
var category: Category?,
val imagePath: String,
)
#Serializable
data class Category(
val id: Long,
val name: String,
val count: Int,
var imagePath: String?,
val page: Int
)
when I want to find images by their id I can't map category attribute
fun getImageById(id: Long): Image {
val images = ArrayList<Image>()
var categoryName = ""
transaction {
Images.select {
Images.id eq id
}.map { row ->
categoryName = row[Images.category]!!
images.add(
Image(
id = row[Images.id].value,
unsplashId = row[Images.unsplashId],
isAvailable = false,
color = row[Images.color],
blurHash = row[Images.blurHash],
category = null,
imagePath = row[Images.imagePath]
)
)
}
images[0].category = categoryRepository.getCategoryByName(categoryName)
}
return images[0]
}
how can I solve this priblem?

Kotlin sealed class with data classes not recognizing subclass

I am trying to define a Kotlin sealed class which consists of a number of data classes. The latter are used to define data transfer objects (DTO) representing the mySQL tables in a room database. I introduced the sealed class to generalize the different DTOs and be able to refer to them all by their supertype (DTO - the common properties each specific DTO has, eg. "id", etc.).
This compiles alright, but I don't think Kotlin understands that the data classes are the "subclasses" of the sealed class - no matter whether I defined them all in the same file as the sealed (parent) class, or - the preferred choice - in the same package... both options should be valid choices, according to the Kotlin documentation.
Any idea, where I'm going wrong here? Thanks.
Code:
package com.tanfra.shopmob.smob.data.local.dto
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.RewriteQueriesToDropUnusedColumns
import com.tanfra.shopmob.smob.data.local.utils.*
/**
* supertype, common to all DTO types - generic part of any DTO class
* (properties declared abstract --> implementation delegated to inheriting concrete class)
*/
sealed class Dto {
abstract val id: String
abstract var itemStatus: SmobItemStatus
abstract var itemPosition: Long
}
#Entity(tableName = "smobGroups")
#RewriteQueriesToDropUnusedColumns
data class SmobGroupDTO(
#PrimaryKey #ColumnInfo(name = "groupId") override val id: String = "invalid smob group entry",
#ColumnInfo(name = "groupItemStatus") override var itemStatus: SmobItemStatus = SmobItemStatus.NEW,
#ColumnInfo(name = "groupItemPosition") override var itemPosition: Long = -1L,
#ColumnInfo(name = "groupName") var name: String = "",
#ColumnInfo(name = "groupDescription") var description: String? = "",
#ColumnInfo(name = "groupType") var type: GroupType = GroupType.OTHER,
#ColumnInfo(name = "groupMembers") var members: List<String> = listOf(),
#ColumnInfo(name = "groupActivityDate") var activityDate: String = "",
#ColumnInfo(name = "groupActivityReps") var activityReps: Long = 0,
) : Dto()
#Entity(tableName = "smobLists")
#RewriteQueriesToDropUnusedColumns
data class SmobListDTO(
#PrimaryKey #ColumnInfo(name = "listId") override val id: String = "invalid smob list id",
#ColumnInfo(name = "listItemStatus") override var itemStatus: SmobItemStatus = SmobItemStatus.NEW,
#ColumnInfo(name = "listItemPosition") override var itemPosition: Long = -1L,
#ColumnInfo(name = "listName") var name: String = "",
#ColumnInfo(name = "listDescription") var description: String? = "",
#ColumnInfo(name = "listItems") var items: List<SmobListItem> = listOf(),
#ColumnInfo(name = "listMembers") var members: List<String> = listOf(),
#ColumnInfo(name = "listLifecycleStatus") var lcStatus: SmobItemStatus = SmobItemStatus.OPEN,
#ColumnInfo(name = "listLifecycleCompletion") var lcCompletion: Double = -1.0,
) : Dto()
#Entity(tableName = "smobProducts")
#RewriteQueriesToDropUnusedColumns
data class SmobProductDTO(
#PrimaryKey #ColumnInfo(name = "productId") override val id: String = "invalid smob product id",
#ColumnInfo(name = "productItemStatus") override var itemStatus: SmobItemStatus = SmobItemStatus.NEW,
#ColumnInfo(name = "productItemPosition") override var itemPosition: Long = -1L,
#ColumnInfo(name = "productName") var name: String = "",
#ColumnInfo(name = "productDescription") var description: String? = "",
#ColumnInfo(name = "productImageUrl") var imageUrl: String? = "",
#ColumnInfo(name = "productCategoryMain") var categoryMain: ProductMainCategory = ProductMainCategory.OTHER,
#ColumnInfo(name = "productCategorySub") var categorySub: ProductSubCategory = ProductSubCategory.OTHER,
#ColumnInfo(name = "productActivityDate") var activityDate: String = "",
#ColumnInfo(name = "productActivityReps") var activityReps: Long = 0L,
#ColumnInfo(name = "productInShopCategory") var inShopCategory: ShopCategory = ShopCategory.OTHER,
#ColumnInfo(name = "productInShopName") var inShopName: String = "dummy shop",
#ColumnInfo(name = "productInShopLocation") var inShopLocation: ShopLocation = ShopLocation(0.0, 0.0),
) : Dto()
#Entity(tableName = "smobShops")
#RewriteQueriesToDropUnusedColumns
data class SmobShopDTO(
#PrimaryKey #ColumnInfo(name = "shopId") override val id: String = "invalid smob shop id",
#ColumnInfo(name = "shopItemStatus") override var itemStatus: SmobItemStatus = SmobItemStatus.NEW,
#ColumnInfo(name = "shopItemPosition") override var itemPosition: Long = -1L,
#ColumnInfo(name = "shopName") var name: String = "",
#ColumnInfo(name = "shopDescription") var description: String? = "",
#ColumnInfo(name = "shopImageUrl") var imageUrl: String? = "",
#ColumnInfo(name = "shopLocationLatitude") var locLat: Double = 0.0,
#ColumnInfo(name = "shopLocationLongitude") var locLong: Double = 0.0,
#ColumnInfo(name = "shopType") var type: ShopType = ShopType.INDIVIDUAL,
#ColumnInfo(name = "shopCategory") var category: ShopCategory = ShopCategory.OTHER,
#ColumnInfo(name = "shopBusiness") var business: List<String> = listOf()
) : Dto()
#Entity(tableName = "smobUsers")
#RewriteQueriesToDropUnusedColumns
data class SmobUserDTO(
#PrimaryKey #ColumnInfo(name = "userId") override val id: String = "invalid smob user id",
#ColumnInfo(name = "userItemStatus") override var itemStatus: SmobItemStatus = SmobItemStatus.NEW,
#ColumnInfo(name = "userItemPosition") override var itemPosition: Long = -1L,
#ColumnInfo(name = "userUsername") var username: String = "",
#ColumnInfo(name = "userName") var name: String = "",
#ColumnInfo(name = "userEmail") var email: String = "",
#ColumnInfo(name = "userImageUrl") var imageUrl: String? = ""
) : Dto()
The reason, I believe Kotlin didn't make the desired connection between the sealed class and the data classes (= subclasses) is that it still asks me for an "else" branch in "when" expressions which act upon the members of the sealed class:
package com.tanfra.shopmob.smob.data.net.nto2dto
import com.tanfra.shopmob.smob.data.local.dto.*
import com.tanfra.shopmob.smob.data.net.nto.*
import com.tanfra.shopmob.smob.data.repo.ato.Ato
// ATO --> DTO
fun <DTO: Dto, ATO: Ato> ATO._asDatabaseModel(d: DTO): DTO? {
return when (d) {
is SmobGroupDTO -> {
SmobGroupDTO(
id = (this as SmobGroupNTO).id,
itemStatus = this.itemStatus,
itemPosition = this.itemPosition,
name = this.name,
description = this.description,
type = this.type,
members = this.members,
activityDate = this.activity.date,
activityReps = this.activity.reps,
) as DTO
}
is SmobListDTO -> {
SmobListDTO(
id = (this as SmobListNTO).id,
itemStatus = this.itemStatus,
itemPosition = this.itemPosition,
name = this.name,
description = this.description,
items = this.items,
members = this.members,
lcStatus = this.lifecycle.status,
lcCompletion = this.lifecycle.completion,
) as DTO
}
is SmobProductDTO -> {
SmobProductDTO(
id = (this as SmobProductNTO).id,
itemStatus = this.itemStatus,
itemPosition = this.itemPosition,
name = this.name,
description = this.description,
imageUrl = this.imageUrl,
categoryMain = this.category.main,
categorySub = this.category.sub,
activityDate = this.activity.date,
activityReps = this.activity.reps,
inShopCategory = this.inShop.category,
inShopName = this.inShop.name,
inShopLocation = this.inShop.location,
) as DTO
}
is SmobShopDTO -> {
SmobShopDTO(
id = (this as SmobShopNTO).id,
itemStatus = this.itemStatus,
itemPosition = this.itemPosition,
name = this.name,
description = this.description,
imageUrl = this.imageUrl,
locLat = this.location.latitude,
locLong = this.location.longitude,
type = this.type,
category = this.category,
business = this.business,
) as DTO
}
is SmobUserDTO -> {
SmobUserDTO(
id = (this as SmobUserNTO).id,
itemStatus = this.itemStatus,
itemPosition = this.itemPosition,
username = this.username,
name = this.name,
email = this.email,
imageUrl = this.imageUrl,
) as DTO
}
else -> null
} // when(DTO) ... resolving generic type to concrete type
}
It's caused by your use of generics on the method signature :
fun <DTO: Dto, ATO: Ato> ATO._asDatabaseModel(d: DTO): DTO?
There's a good thread on Reddit which is very like your example. See here:
https://www.reddit.com/r/Kotlin/comments/ei8zh5/kotlin_requires_else_branch_in_when_statement/
So, to solve your problem, just change the method signature to return a type of DTO not DTO?
It's almost as if the compiler is forgetting that the DTO is a sealed class when you make it a generic parameter, so you need an exhaustive check.
As you as using is in a when statement Kotlin will smart cast the DTO to the right type anyway, so no need for the generic argument.
Here's a cut down example based on your code that works without the else:
package paul.sealed
sealed class DTO {
abstract val id: String
}
data class SmobGroupDTO(override val id: String = "invalid smob user id", val name: String = "") : DTO()
data class SmobListDTO(override val id: String = "invalid smob user id", val name: String = "") : DTO()
fun main() {
fun processDTO(dto: DTO): String {
return when (dto) {
is SmobGroupDTO -> "Group"
is SmobListDTO -> "List"
}
}
}

Retrieve data from api and put into room

I'm facing this error when putting data into room from my api:
java.lang.RuntimeException: Unable to invoke no-args constructor for
retrofit2.Call<com.example.youbank.models.Customer>. Registering an
InstanceCreator with Gson for this type may fix this problem.
I have looked up this issue and tried multiple things to fix it but, i think there is something else wrong, which probably comes down to my lack of knowledge on this subject.
Sorry for the big copypaste of my code but i don't know where to fix this problem so im just including what i think is needed.
HomeScreenFragment:
class HomeScreenMotionFragment: Fragment(), CoroutineScope {
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
private var _binding: FragmentHomeScreenMotionBinding? = null
private val binding get() = _binding!!
private val vm: CustomerViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launch {
vm.addCustomerToRoomDB(14)
}
}
CustomerViewModel:
class CustomerViewModel(application: Application): AndroidViewModel(application) {
val readCustomer: LiveData<RoomCustomer>
val readAccount: LiveData<List<RoomAccount>>
val readCard: LiveData<List<RoomCard>>
private val customerRepo: CustomerRepository
private val accountRepo: AccountRepository
private val cardRepo: CardRepository
var cus: Customer
var a = listOf<Account>()
var cards = listOf<Card>()
init {
cus = Customer()
val customerDao = CustomerDatabase.getDatabase(application).customerDao()
customerRepo = CustomerRepository(customerDao)
readCustomer = customerRepo.readCustomer
val accountDao = CustomerDatabase.getDatabase(application).accountDao()
accountRepo = AccountRepository(accountDao)
readAccount = accountRepo.readAccounts
val cardDao = CustomerDatabase.getDatabase(application).cardDao()
cardRepo = CardRepository(cardDao)
readCard = cardRepo.readCards
}
suspend fun addCustomerToRoomDB(id: Int) {
val service: CustomerService = ApiService.buildService(CustomerService::class.java)
val req: Call<Customer> = service.getCustomerById(id)
req.enqueue(object: Callback<Customer> {
override fun onResponse(call: Call<Customer>, response: Response<Customer>) {
cus = response.body()!!
a = response.body()!!.accounts
cards = response.body()!!.accounts[0].cards
}
override fun onFailure(call: Call<Customer>, t: Throwable) {
Log.d("get customer failed", t.cause.toString())
}
})
val roomCustomer = RoomCustomer(
0, cus.customerId, cus.fullName, cus.phone, cus.address, cus.birthday.toString(), cus.email, cus.password)
val roomAccount = RoomAccount(0, a[0].accountId, a[0].accountNumber, a[0].accountType, a[0].balance)
val roomCard = RoomCard(
0, cards[0].cardId, cards[0].cardNumber, cards[0].ccv, cards[0].expirationDate, cards[0].cardType,
cards[0].cardStatus)
// Adding customer to roomdatabase
customerRepo.addCustomer(roomCustomer)
accountRepo.addAccounts(roomAccount)
cardRepo.addCards(roomCard)
}
}
CustomerService:
interface CustomerService {
#GET("Customers/{id}")
suspend fun getCustomerById(#Path("id") id: Int): Call<Customer>
}
My models:
class Customer {
var customerId: Int = -1
var fullName: String = ""
var phone: String = ""
var address: String = ""
var birthday: Date? = null
var email: String = ""
var password: String = ""
lateinit var accounts: List<Account>
}
class Account {
var accountId: Int = -1
var accountNumber: String = generateAccNumber()
lateinit var accountType: AccountType
var balance: Double = 0.0
lateinit var cards: List<Card>
}
class Card {
var cardId: Int = -1
var cardNumber: Int = -1
var ccv: Int = -1
lateinit var expirationDate: String
lateinit var cardType: CardType
lateinit var cardStatus: CardStatus
}
My repositories:
class CustomerRepository(private val customerDao: CustomerDao) {
val readCustomer: LiveData<RoomCustomer> = customerDao.getCustomer()
fun addCustomer(c: RoomCustomer) {
customerDao.addCustomer(c)
}
}
class AccountRepository (private val accountDao: AccountDao) {
val readAccounts: LiveData<List<RoomAccount>> = accountDao.getAccounts()
fun addAccounts(a: RoomAccount) {
accountDao.addAccount(a)
}
}
class CardRepository(private val cardDao: CardDao) {
val readCards: LiveData<List<RoomCard>> = cardDao.getCards()
fun addCards(c: RoomCard) {
cardDao.addCards(c)
}
}
My daos:
#Dao
interface CustomerDao {
#Query("SELECT * FROM customer_table")
fun getCustomer(): LiveData<RoomCustomer>
#Insert
fun addCustomer(c: RoomCustomer)
#Delete
fun deleteCustomer(c: RoomCustomer)
}
#Dao
interface AccountDao {
#Query("SELECT * FROM accounts_table")
fun getAccounts(): LiveData<List<RoomAccount>>
#Insert
fun addAccount(a: RoomAccount)
#Delete
fun deleteAccount(a: RoomAccount)
}
#Dao
interface CardDao {
#Query("SELECT * FROM cards_table")
fun getCards(): LiveData<List<RoomCard>>
#Insert
fun addCards(c: RoomCard)
#Delete
fun deleteCard(c: RoomCard)
}
My room models:
#Entity(tableName = "customer_table")
data class RoomCustomer(
#PrimaryKey(autoGenerate = true)
val CID: Int,
val customerId: Int,
val fullName: String,
val phone: String,
val address: String,
val birthday: String,
val email: String,
val password: String
)
#Entity(tableName = "accounts_table")
data class RoomAccount(
#PrimaryKey(autoGenerate = true)
val AID: Int,
val accountId: Int,
val accountNumber: String,
val accountType: AccountType,
val balance: Double
)
#Entity(tableName = "cards_table")
data class RoomCard(
#PrimaryKey(autoGenerate = true)
val CID: Int,
val cardId: Int,
val cardNumber: Int,
val ccv: Int,
val expirationDate: String,
val cardType: CardType,
val cardStatus: CardStatus
)

Room generate id with constructor

I am usind Room DB and I have class which I need to generate id:
#Entity
data class TaskEntity(
#PrimaryKey(autoGenerate = true) var uid: Long?,
#ColumnInfo(name = "title") var title: String?,
#ColumnInfo(name = "start") var startTime: Long?,
#ColumnInfo(name = "duration") var duration: Long?,
): Serializable {
constructor(
) : this(null, "", 0, 0)
}
In the activity I will init the calss and set parameters at runtime:
var task: TaskEntity
task = TaskEntity()
task.name = cal.name
....
Dao class:
#Dao
interface TasksDao {
#Query("SELECT * FROM taskentity ORDER BY startTime ASC")
fun getAllTasks(): List<TaskEntity>
#Insert
fun insert(item: TaskEntity)
#Delete
fun delete(item: TaskEntity)
#Update
fun update(item: TaskEntity)
}
But the ID is always null as in the constructor. I have seen in some documentation in case its null it will be auto generated. What am I missing?
You have manually created the TaskEntity object, so it's uid is null
Also null or 0 initial value is allowed for integer field with primary key and autoGenerate flag
So i guess it is better to write
#PrimaryKey(autoGenerate = true) var uid: Long,
// ...
) : this(0L, "", 0, 0)

Deep merging data classes in Kotlin

How can I do a recursive / deep merge of two data classes in Kotlin? Something like this:
import kotlin.reflect.*
import kotlin.reflect.full.*
data class Address(
val street: String? = null,
val zip: String? = null
)
data class User(
val name: String? = null,
val age: Int? = null,
val address: Address? = null
)
inline fun <reified T : Any> T.merge(other: T): T {
val nameToProperty = T::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = T::class.primaryConstructor!!
val args = primaryConstructor.parameters.associate { parameter ->
val property = nameToProperty[parameter.name]!!
val type = property.returnType.classifier as KClass<*>
if (type.isData) {
parameter to this.merge(other) //inline function can't be recursive
} else {
parameter to (property.get(other) ?: property.get(this))
}
}
return primaryConstructor.callBy(args)
}
val u1 = User(name = "Tiina", address = Address(street = "Hämeenkatu"))
val u2 = User(age = 23, address = Address(zip = "33100"))
u1.merge(u2)
// expected: User(age = 23, name= "Tiina", address = Address(zip = "33100", street = "Hämeenkatu")
related: Combining/merging data classes in Kotlin
There were several problems in the posted code,
unnecessary reification and inlining
when type isData was detected instead of merging the values of the property merge on this with the other was called, so it became endless recursion.
get cannot be used on KProperty1<out T, Any?> because of the variance
some non-idiomatic stuff which works, but can be made better
Here's the fixed version. For production I would've added some checks and error messages, but this should work for "happy path" and hopefully give you the base to build on:
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.primaryConstructor
data class Address(
val street: String? = null,
val zip: String? = null
)
data class User(
val name: String? = null,
val age: Int? = null,
val address: Address? = null,
val map: Map<String, Int>? = null
)
fun <T> mergeData(property: KProperty1<out T, Any?>, left: T, right: T): Any? {
val leftValue = property.getter.call(left)
val rightValue = property.getter.call(right)
return rightValue?.let {
if ((property.returnType.classifier as KClass<*>).isSubclassOf(Map::class)) (leftValue as? Map<*, *>)?.plus(it as Map<*, *>)
else leftValue?.merge(it)
} ?: rightValue ?: leftValue
}
fun <T> lastNonNull(property: KProperty1<out T, Any?>, left: T, right: T) =
property.getter.call(right) ?: property.getter.call(left)
fun <T : Any> T.merge(other: T): T {
val nameToProperty = this::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = this::class.primaryConstructor!!
val args: Map<KParameter, Any?> = primaryConstructor.parameters.associateWith { parameter ->
val property = nameToProperty[parameter.name]!!
val type = property.returnType.classifier as KClass<*>
when {
type.isData || type.isSubclassOf(Map::class) -> mergeData(property, this, other)
else -> lastNonNull(property, this, other)
}
}
return primaryConstructor.callBy(args)
}
// verification
val u1 = User(name = "Tiina", address = Address(street = "Hämeenkatu"), map = mapOf("a" to 1))
val u2 = User(age = 23, address = Address(zip = "33100"), map = mapOf("b" to 2))
check(
u1.merge(u2) == User(
age = 23,
name = "Tiina",
address = Address(zip = "33100", street = "Hämeenkatu"),
map = mapOf("a" to 1,"b" to 2)
)
) {
"doesn't work"
}
println("Works!")