My DB table is as
#Entity
internal data class ConversationEntity(
#PrimaryKey val conversationId: String,
val status: String,
val createdAt: String,
val modifiedAt: String,
val lastMessage: String? = null,
val feedbackType: ChatFeedbackType? = null
)
I want to use #Ignore on lastMessage but it gives me below error.
#Entity
internal data class ConversationEntity(
#PrimaryKey val conversationId: String,
val status: String,
val createdAt: String,
val modifiedAt: String,
#Ignore
val lastMessage: String? = null,
val feedbackType: ChatFeedbackType? = null)
The error I get
Entities and POJOs must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type).
What is wrong here?
My dao is
#Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveConversations(entities: List<ConversationEntity>)
My current DB version is 6. Would I also need to write migration query for adding ignore?
When using #Ignore the #Ignore is not included in the table thus Room expects a constructor that it can use for inserting/extracting rows rather than the full default constructor.
As such you need to add a suitable constructor that doesn't include the lastMessage
After resolving the constructor issue you will then, I believe, encounter an issue as Conversation isn't public. You can resolve this by removing the internal keyword.
e.g.
#Entity
data class ConversationEntity(
#PrimaryKey val conversationId: String,
val status: String,
val createdAt: String,
val modifiedAt: String,
#Ignore
val lastMessage: String? = null,
val feedbackType: ChatFeedbackType? = null) {
constructor(
conversationId: String,
status: String,
createdAt: String,
modifiedAt: String,
feedbackType: ChatFeedbackType):
this(conversationId,status,createdAt,modifiedAt,null,feedbackType)
}
this assumes that you have a TypeConverter for ChatFeedbackType
As an example of the above (using String for feedbackType for convenience)
and a dao:-
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(entities: List<ConversationEntity>)
and using:-
val conversations = listOf<ConversationEntity>(
ConversationEntity("Conv1","blah","2021-06-25 09:00:00","2021-06-25 10:00:00","NONE"),
ConversationEntity("Conv2","blah","2021-06-25 09:00:00","2021-06-25 10:00:00","NONE"),
ConversationEntity("Conv1","blah","2021-06-26 09:00:00","2021-06-26 10:00:00","NONE")
)
dao.insert(conversations)
Then the ConversationEntity table after running is:-
i.e. the two rows have been added and the first replaced.
HOWEVER
Ignoring a field/variable makes it so that Room will never populate that value so effectively it makes the ignored fields pretty useless being coded in an entity.
As an example if your wanted to populate lastMessage by calculating it based upon a query say for example purposes:-
#Query("SELECT *, 'calculated data' AS lastMessage FROM conversationentity")
fun getConversations(): List<ConversationEntity>
which you may think would populate the ignored lastMessage field/variable with the value calculated data (literal rather than an expression (which could be a sub-query))
a literal has been used for convenience/brevity
However Room will issue a warning e.g. warning: The query returns some columns [lastMessage] which are not used by .... and as the constructor used/selected by room is incomplete constructor added when extracting the ConversationEntity the calculated value is not paced into the lastMessage field/variable (even though it is available).
I would suggest, if the intention is to populate the ignored values from the database that you consider the entity as a partial class and have a POJO as the complete class that Embeds the Entity. e.g.
Have ConversationEntity as :-
#Entity
data class ConversationEntity(
#PrimaryKey val conversationId: String,
val status: String,
val createdAt: String,
val modifiedAt: String,
val feedbackType: String? = null) {
}
String instead of ChatFeedbackType for the demo/example
and then have a POJO e.g.
data class ConversationEntityPojo (
#Embedded
var conversationEntity: ConversationEntity,
var lastMessage: String?
)
so as you see no need for the constructor nor for the #Ignore, but instead the more complete POJO.
Following on from the previous code used for testing but now with the modified Entity and POJO then using the following #Query's for demonstration :-
#Query("SELECT * FROM conversationentity")
fun getConversationsEntityOnly(): List<ConversationEntity>
#Query("SELECT *, 'calculated data' AS lastMessage FROM conversationentity")
fun getConversationsEntityOnlyWithLastMessage(): List<ConversationEntity>
#Query("SELECT * FROM conversationentity")
fun getConversationsPOJO(): List<ConversationEntityPojo>
#Query("SELECT *,'calculated data' AS lastMessage FROM conversationentity")
fun getConversationPOJOWithLastMessage(): List<ConversationEntityPojo>
and then using :-
.... code from previous example
dao.insert(conversations)
for(c: ConversationEntity in dao.getConversationsEntityOnly()) {
Log.d("CONVINFO",
"Conversation is ${c.conversationId} " +
"status is ${c.status} " +
"created ${c.createdAt} " +
"modified ${c.modifiedAt} "
// CANT DO THIS AS ENTITY doesn't have lastMessage field "lastmessage ${c.lastMessage}"
)
}
for (c: ConversationEntity in dao.getConversationsEntityOnlyWithLastMessage()) {
Log.d("CONVINFO",
"Conversation is ${c.conversationId} " +
"status is ${c.status} " +
"created ${c.createdAt} " +
"modified ${c.modifiedAt} "
// CANT DO THIS AS ENTITY doesn't have lastMessage field "lastmessage ${c.lastMessage}"
)
}
for(c: ConversationEntityPojo in dao.getConversationsPOJO()) {
Log.d("CONVINFO",
"Conversation is ${c.conversationEntity.conversationId} " +
"status is ${c.conversationEntity.status} " +
"created ${c.conversationEntity.createdAt} " +
"modified ${c.conversationEntity.modifiedAt} " +
"lastmessage ${c.lastMessage}") /* Will not be populated as lastMessage is not available in extracted data*/
}
for(c: ConversationEntityPojo in dao.getConversationPOJOWithLastMessage()) {
Log.d("CONVINFO",
"Conversation is ${c.conversationEntity.conversationId} " +
"status is ${c.conversationEntity.status} " +
"created ${c.conversationEntity.createdAt} " +
"modified ${c.conversationEntity.modifiedAt} " +
"lastmessage ${c.lastMessage}") /* can be used */
}
Then the result is :-
2021-06-27 12:26:38.919 D/CONVINFO: Conversation is Conv2 status is blah created 2021-06-25 09:00:00 modified 2021-06-25 10:00:00
2021-06-27 12:26:38.919 D/CONVINFO: Conversation is Conv1 status is blah created 2021-06-26 09:00:00 modified 2021-06-26 10:00:00
2021-06-27 12:26:38.920 D/CONVINFO: Conversation is Conv2 status is blah created 2021-06-25 09:00:00 modified 2021-06-25 10:00:00
2021-06-27 12:26:38.920 D/CONVINFO: Conversation is Conv1 status is blah created 2021-06-26 09:00:00 modified 2021-06-26 10:00:00
2021-06-27 12:26:38.924 D/CONVINFO: Conversation is Conv2 status is blah created 2021-06-25 09:00:00 modified 2021-06-25 10:00:00 lastmessage null
2021-06-27 12:26:38.924 D/CONVINFO: Conversation is Conv1 status is blah created 2021-06-26 09:00:00 modified 2021-06-26 10:00:00 lastmessage null
2021-06-27 12:26:38.926 D/CONVINFO: Conversation is Conv2 status is blah created 2021-06-25 09:00:00 modified 2021-06-25 10:00:00 lastmessage calculated data
2021-06-27 12:26:38.927 D/CONVINFO: Conversation is Conv1 status is blah created 2021-06-26 09:00:00 modified 2021-06-26 10:00:00 lastmessage calculated data
use this class for entity
class ConversationEntity (){
#PrimaryKey
val conversationId: String
val status: String
val createdAt: String
val modifiedAt: String
#Ignore
val lastMessage: String? = null
val feedbackType: ChatFeedbackType? = null
}
Related
I'm trying to implement caching of a JSON API response with Room.
The response I get in JSON follows this data class structure:
#Serializable
data class ApiDataResponse(
val success: Boolean,
val message: String? = null,
val albums: List<AlbumResponse> = emptyList()
)
#Serializable
data class AlbumResponse(
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>,
val pics: List<PicResponse>
)
#Serializable
data class PicResponse(
val picUrl: String,
val emojis: List<String>
)
Notes:
#Serializable is from kotlinx.serialization library to parse the JSON response.
These response data classes are only used inside my datasource layer, the view layer doesn't care about an ApiDataResponse and only knows a "pure" version of AlbumResponse called Album and a "pure" version of PicResponse called Pic (by "pure" I mean a data class without external library annotations).
So to implement this cache with Room I could discard the ApiDataResponse and save only the contents of AlbumResponse (and consequently PicResponse), having new data classes for Room entities following this idea:
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>, // obstacle here
val pics: List<PicEntity> // obstacle here
)
// obstacle here
// #Entity
data class PicEntity(
val picUrl: String,
val emojis: List<String>
)
I already know how to save simple data in Room, with the simplest JSON I was able to do this task, the problem is that in this more complex scenario I have no idea how to achieve this goal. So I wish someone could guide me in this situation.
Maybe it's a little late, but I would still like to add some interesting information regarding MikeT's answer.
It is not necessary to create a new data class just to transform a custom object into a JSON with TypeConverter, for example:
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>,
val pics: List<PicEntity> // can be converted directly
)
import kotlinx.serialization.Serializable
#Serializable // to be able to do the serialize with the kotlinx.serialization
data class PicEntity(
val picUrl: String,
val emojis: List<String>
)
With just these two data classes we can build the TypeConverters as follows:
import androidx.room.TypeConverter
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class DatabaseConverter {
private val json = Json
#TypeConverter
fun convertStringListToString(strings: List<String>): String =
json.encodeToString(strings)
#TypeConverter
fun convertStringToStringList(string: String): List<String> =
json.decodeFromString(string)
#TypeConverter
fun convertPicEntityListToString(picsEntity: List<PicEntity>): String =
json.encodeToString(picsEntity)
#TypeConverter
fun convertStringToPicEntityList(string: String): List<PicEntity> =
json.decodeFromString(string)
}
Code to create an example dummy list:
object DummyAlbums {
fun createList(): List<AlbumEntity> = listOf(
AlbumEntity(
id = "0001",
title = "Album AB",
createdBy = "Created by AB",
enabled = true,
keywords = listOf("ab"),
pics = dummyPics(albumId = "0001", size = 0)
),
AlbumEntity(
id = "0002",
title = "Album CD",
createdBy = "Created by CD",
enabled = false,
keywords = listOf("cd", "c", "d"),
pics = dummyPics(albumId = "0002", size = 1)
),
AlbumEntity(
id = "0003",
title = "Album EF",
createdBy = "Created by EF",
enabled = true,
keywords = listOf(),
pics = dummyPics(albumId = "0003", size = 2)
)
)
private fun dummyPics(
albumId: String,
size: Int
) = List(size = size) { index ->
PicEntity(
picUrl = "url.com/$albumId/${index + 1}",
emojis = listOf(":)", "^^")
)
}
}
So we can have the following data in table:
I wanted to highlight this detail because maybe it can be important for someone to have a table with the cleanest data. And in even more specific cases, to have it clean, you can do the conversion manually using Kotlin functions, such as joinToString(), split(), etc.
I believe the issue is with columns as lists.
What you could do is add the following classes so the Lists are embedded within a class:-
data class StringList(
val stringList: List<String>
)
data class PicEntityList(
val picEntityList: List<PicEntity>
)
and then change AlbumEntity to use the above instead of the Lists, as per:-
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
//val keywords: List<String>, // obstacle here
val keywords: StringList, /// now not an obstacle
//val pics: List<PicEntity> // obstacle here
val emojis: PicEntityList// now not an obstacle
)
To be able to store the "complex" (single object) you need to convert this so some TypeConverters e.g.
class RoomTypeConverters{
#TypeConverter
fun convertStringListToJSON(stringList: StringList): String = Gson().toJson(stringList)
#TypeConverter
fun convertJSONToStringList(json: String): StringList = Gson().fromJson(json,StringList::class.java)
#TypeConverter
fun convertPicEntityListToJSON(picEntityList: PicEntityList): String = Gson().toJson(picEntityList)
#TypeConverter
fun convertJSONToPicEntityList(json: String): PicEntityList = Gson().fromJson(json,PicEntityList::class.java)
}
note this utilises the dependency com.google.code.gson
You then need to have the #TypeConverters annotation to cover the appropriate scope (at the #Database level is the most scope). Note the plural rather than singular, they are different.
To demonstrate the above works, First some functions in an interface annotated with #Dao :-
#Dao
interface AlbumDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(albumEntity: AlbumEntity): Long
#Query("SELECT * FROM albums")
fun getAllAlbums(): List<AlbumEntity>
}
Second an #Database annotated class (note the #TypeConverters annotation) :-
#TypeConverters(RoomTypeConverters::class)
#Database(entities = [AlbumEntity::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAlbumDao(): AlbumDao
companion object {
#Volatile
private var instance: TheDatabase?=null
fun getInstance(context: Context): TheDatabase {
if (instance==null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"album.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
Third some activity code to actually do something (insert some Albums and then extract them writing the extracted data to the Log) :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AlbumDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAlbumDao()
dao.insert(AlbumEntity(
"Album001", "The First Album","Fred",false,
StringList(listOf("The","First","Album")),
PicEntityList(
listOf(
PicEntity("PE001", listOf("emoji1","emoji2","emoji3")),
PicEntity("PE002",listOf("emoji10")),
PicEntity("PE003", listOf("emoji20","emoji21"))
))
))
dao.insert(AlbumEntity(
"Album002","This is the Second Album","Mary", true,
StringList(listOf("keya","keyb","keyc","keyd","keye")),
PicEntityList(
listOf(
PicEntity("PE011", listOf("emoji30","emoji31")),
PicEntity("PE012", listOf("emoji1","emoji10","emoji20","emoji30"))
))
))
for (a in dao.getAllAlbums()) {
logAlbum(a)
}
}
fun logAlbum(albumEntity: AlbumEntity) {
val keywords = StringBuilder()
for(s in albumEntity.keywords.stringList) {
keywords.append("\n\t$s")
}
val pelog = StringBuilder()
for (pe in albumEntity.emojis.picEntityList) {
pelog.append("\n\tURL is ${pe.picUrl}")
for (emoji in pe.emojis) {
pelog.append("\n\t\tEmoji is ${emoji}")
}
}
Log.d(
"ALBUMINFO",
"Album id is ${albumEntity.id} " +
"Title is ${albumEntity.title} " +
"CreateBy ${albumEntity.createdBy} " +
"Enabled=${albumEntity.enabled}. " +
"It has ${albumEntity.keywords.stringList.size} keywords. " +
"They are $keywords\n. " +
"It has ${albumEntity.emojis.picEntityList.size} emojis. " +
"They are ${pelog}"
)
}
}
Run on the main thread for convenience and brevity
When run then the log contains:-
D/ALBUMINFO: Album id is Album001 Title is The First Album CreateBy Fred Enabled=false. It has 3 keywords. They are
The
First
Album
. It has 3 emojis. They are
URL is PE001
Emoji is emoji1
Emoji is emoji2
Emoji is emoji3
URL is PE002
Emoji is emoji10
URL is PE003
Emoji is emoji20
Emoji is emoji21
D/ALBUMINFO: Album id is Album002 Title is This is the Second Album CreateBy Mary Enabled=true. It has 5 keywords. They are
keya
keyb
keyc
keyd
keye
. It has 2 emojis. They are
URL is PE011
Emoji is emoji30
Emoji is emoji31
URL is PE012
Emoji is emoji1
Emoji is emoji10
Emoji is emoji20
Emoji is emoji30
i.e. the 2 albums have been extracted along with the appropriate embedded lists.
The Albums table itself (via App Inspection) consists of :-
An Alternative, and from a Database perspective, better approach, instead of embedding lists as a single value (String), would have the lists as related tables (with a one-many or a many-many relationship).
I am rewriting my old Sqlite Android app that was in Java to be a Jetpack Compose app in Kotlin that uses a Room database.
I've got about half of the app done but now I am seeing a strange behavior where my DAO query is not returning the data it should be, and the cause seems to be because the correct constructor, defined in my data model class, is not being called.
I am pretty sure this constructor WAS being called back before, before I added a new table to the database. I'm not 100% on this but I think so.
Anyway, here's some relevant code:
Database:
Data Model (I've added an #Ignore property, firearmImageUrl, for this imageFile column from the firearm_image table so it's part of the Firearm object. Maybe not the best way to do this, for joining tables? But this is a small simple app that like 5 people worldwide might use, more likely just me):
#Entity(tableName = "firearm")
class Firearm {
#ColumnInfo(name = "_id")
#PrimaryKey(autoGenerate = true)
var id = 0
var name: String = ""
var notes: String? = null
#Ignore
var shotCount = 0
#Ignore
var firearmImageUrl: String = ""
#Ignore
constructor() {
}
#Ignore
constructor(
name: String,
notes: String?
) {
this.name = name
this.notes = notes
}
#Ignore
constructor(
name: String,
notes: String?,
shotCount: Int
) {
this.name = name
this.notes = notes
this.shotCount = shotCount
}
#Ignore
constructor(
id: Int,
name: String,
notes: String?,
shotCount: Int
) {
this.id = id
this.name = name
this.notes = notes
this.shotCount = shotCount
}
// THIS IS THE CONSTRUCTOR THAT I **WANT** TO BE CALLED AND IS NOT. THIS USED TO HAVE AN
// #IGNORE TAG ON IT BUT REMOVING IT DID NOTHING
constructor(
id: Int,
name: String,
notes: String?,
shotCount: Int,
firearmImageUrl: String
) {
this.id = id
this.name = name
this.notes = notes
this.shotCount = shotCount
this.firearmImageUrl = firearmImageUrl
}
// THIS IS THE CONSTRUCTOR THAT IS BEING CALLED BY THE BELOW DAO METHOD, EVEN THOUGH
// ITS PARAMETERS DO NOT MATCH WHAT'S BEING RETURNED BY THAT QUERY
constructor(
id: Int,
name: String,
notes: String?,
) {
this.id = id
this.name = name
this.notes = notes
}
}
DAO (I removed the suspend keyword just so this thing would hit a debug breakpoint; also this query absolutely works, I copy-pasted it into the Database Inspector and ran it against the db and it returns the proper data with firearmImageUrl populated with a path):
#Query(
"SELECT f._id, " +
"f.name, " +
"f.notes, " +
"CASE WHEN SUM(s.roundsFired) IS NULL THEN 0 " +
"ELSE SUM(s.roundsFired) " +
"END shotCount, " +
"fi.imageFile firearmImageUrl " +
"FROM firearm f " +
"LEFT JOIN shot_track s ON f._id = s.firearmId " +
"LEFT JOIN firearm_image fi ON f._id = fi.firearmId " +
"WHERE f._id = :firearmId " +
"GROUP BY f._id " +
"ORDER BY f.name"
)
fun getFirearm(firearmId: Int): Firearm?
Repo:
override fun getFirearm(firearmId: Int): Firearm? {
return dao.getFirearm(firearmId)
}
Use Case (I'm dumb and decided to do this Clean Architecture but it's way overkill; this is just an intermediate class and calls the Repo method):
data class FirearmUseCases(
/**
* Gets the valid Firearms in the application.
*/
val getFirearms: GetFirearms,
/**
* Gets the specified Firearm.
*/
val getFirearm: GetFirearm
)
class GetFirearm(private val repository: FirearmRepository) {
operator fun invoke(firearmId: Int): Firearm? {
return repository.getFirearm(firearmId)
}
}
ViewModel:
init {
savedStateHandle.get<Int>("firearmId")?.let { firearmId ->
if (firearmId > 0) {
viewModelScope.launch {
firearmUseCases.getFirearm(firearmId)?.also { firearm ->
_currentFirearmId.value = firearm.id
// and so on... point is, the object is retrieved in this block
}
}
}
}
}
What's happening is the DAO is calling the constructor that I've commented above, and not the constructor that has the parameters that match what the query is returning. Not sure why. That constructor did have an #Ignore tag on it before tonight but I just tried removing it and there was no difference; constructor with only 3 parameters is still being called.
Thanks for any help, this Room stuff is nuts. I should've just stuck with Sqlite lmao. It's such a simple app, the old version was super fast and worked fine. Silly me wanting to learn contemporary design though.
I believe that your issue is based upon shotCount being #Ignored (which you obviously want). Thus, even though you have it in the output, Room ignores the column and thus doesn't use the constructor you wish.
I would suggest that the resolution is quite simple albeit perhaps a little weird and that is to have Firearm not annotated with #Entity and just a POJO (with no Room annotation) and then have a separate #Entity annotated class specifically for the table.
You could obviously add constructors/functions, as/if required to the Firearm class to handle FirearmTable's
e.g.
#Entity(tableName = "firearm")
data class FireArmTable(
#ColumnInfo(name = BaseColumns._ID)
#PrimaryKey
var id: Long?=null,
var name: String,
var notes: String? = null
)
using BaseColumns._ID would change the ID column name should it ever change.
using Long=null? without autogenerate = true will generate an id (if no value is supplied) but is more efficient see https://sqlite.org/autoinc.html (especially the very first sentence)
the above are just suggestions, they are not required
and :-
class Firearm() : Parcelable {
#ColumnInfo(name = "_id")
#PrimaryKey(autoGenerate = true)
var id = 0
var name: String = ""
var notes: String? = null
//#Ignore
var shotCount = 0
//#Ignore
var firearmImageUrl: String = ""
....
Using the above and using (tested with .allowMainThreadQueries) then the following:-
db = TheDatabase.getInstance(this)
dao = db.getFirearmDao()
val f1id = dao.insert(FireArmTable( name = "F1", notes = "Awesome"))
val f2id = dao.insert(FireArmTable(name = "F2", notes = "OK"))
dao.insert(Firearm_Image(firearmId = f1id, imageFile = "F1IMAGE"))
dao.insert(Shot_track(firearmId = f1id, roundsFired = 10))
dao.insert(Shot_track(firearmId = f1id, roundsFired = 20))
dao.insert(Shot_track(firearmId = f1id, roundsFired = 30))
dao.insert(Firearm_Image(firearmId = f2id, imageFile = "F2IMAGE"))
dao.insert(Shot_track(firearmId = f2id, roundsFired = 5))
dao.insert(Shot_track(firearmId = f2id, roundsFired = 15))
logFirearm(dao.getFirearm(f1id.toInt()))
val f1 = dao.getFirearm(f1id.toInt())
val f2 = dao.getFirearm(f2id.toInt())
logFirearm(f2)
}
fun logFirearm(firearm: Firearm?) {
Log.d("FIREARMINFO","Firearm: ${firearm!!.name} Notes are: ${firearm.notes} ImageURL: ${firearm.firearmImageUrl} ShotCount: ${firearm.shotCount}")
}
Where getFirearm is your Query copied and pasted, shows the following in the log:-
D/FIREARMINFO: Firearm: F1 Notes are: Awesome ImageURL: F1IMAGE ShotCount: 60
D/FIREARMINFO: Firearm: F2 Notes are: OK ImageURL: F2IMAGE ShotCount: 20
i.e. Shotcounts as expected.
I have an Room Entity called City:
#Entity(tableName = "cities")
class City(
#PrimaryKey
#ColumnInfo(name = "unique_city_id")
val id: Long,
#ColumnInfo(name = "city_name")
val name: String,
#ColumnInfo(name = "city_code")
val code: String,
)
And I have a list of objects of this class type:
data class CoffeeHouse(
override val id: Long,
override val latitude: Double,
override val longitude: Double,
override val city: City?,
override val address: String,
)
I need to save both CoffeeHouse and City classes. Because there are a lot of identical cities, I map a list of coffeehouses to a set of cities to get only unique ones:
val cities = coffeeHouses.map { it.city?.toPersistenceType() }.toSet()
(.toPersistenceType() just maps domain type to persistence)
And then I'm inserting coffeeHouses and cities into Room Database using these DAOs:
#Dao
abstract class CoffeeHouseDao(val cacheDatabase: CacheDatabase) {
private val cityDao = cacheDatabase.cityDao()
#Insert(onConflict = REPLACE)
abstract suspend fun insertAllCoffeeHouses(coffeeHouses: List<CoffeeHouse>)
#Transaction
open suspend fun insertAllCoffeeHousesInfo(
coffeeHouses: List<CoffeeHouse>,
cities: Set<City?>,
) {
insertAllCoffeeHouses(coffeeHouses)
cityDao.setCities(cities)
}
}
#Dao
interface CityDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun setCities(cities: Set<City?>)
The problem is when I'm trying to insert Set<City?> app crashes with an exception:
Uncaught exception java.lang.NullPointerException:
Attempt to invoke virtual method 'long com.coffeeapp.android.persistence.entity.City.getId()'
on a null object reference
Stacktrace points on the line of cities insertion, so I don't understand how to make it right.
This is happening because you have set the ID field in city as the Primary Key for that table and it cannot be null.
You can try changing your annotation to
#PrimaryKey(autoGenerate = true)
Or if you do not want auto increment you have to make sure that the id is not null whenever you are inserting a City.
I think it is because of the city: City? and cities: Set<City?> in the CofeeHouse entity. Try to make them not nullable.
To allow for inserting a with null you can use :-
#Entity(tableName = "cities")
class City(
#PrimaryKey
#ColumnInfo(name = "unique_city_id")
val id: Long?, //<<<<<<<< ? ADDED
#ColumnInfo(name = "city_name")
val name: String,
#ColumnInfo(name = "city_code")
val code: String,
)
As such id will be generated when it is inserted. e.g. the following (based upon for reduced for convenience).
However, the REPLACE conflict strategy will never result in replacement as null will generate a unique id.
What I believe you want is that either city name, the city code or both (together or independently) constitutes a unique entry.
As such :-
#Entity(
tableName = "cities",
indices = [
/*
probably just one of these all three is overkill
*/
Index(value = ["city_name"],unique = true),
Index(value = ["city_code"], unique = true),
Index(value = ["city_name","city_code"],unique = true)
]
)
class City(
#PrimaryKey
#ColumnInfo(name = "unique_city_id")
val id: Long?,
#ColumnInfo(name = "city_name")
val name: String,
#ColumnInfo(name = "city_code")
val code: String,
)
As an example consider the following :-
cityDao.setCities(setOf<City>(City(null,"Sydney","SYD1"),City(null,"New York","NY1")))
cityDao.setCities(setOf<City>(City(null,"Sydney","SYD1"),City(null,"New York","NY1")))
So an attempt is made to add the same set of cities The result is:-
i.e. The first added Sydney and New York with id's 1 and 2, the second attempt replaced due to the conflict which deletes the originals so you end up with id's 3 and 4. Without the unique index(s) then the result would have been 4 rows with id's 1,2,3 and 4.
I started using Room database and went through several docs to create room entities.
These are my relations. A Chat Channel can have Many Conversations. So this goes as one-to-many relationship. Hence i created entities as below.
Channel Entity
#Entity(primaryKeys = ["channelId"])
#TypeConverters(TypeConverters::class)
data class Channel(
#field:SerializedName("channelId")
val channelId: String,
#field:SerializedName("channelName")
val channelName: String,
#field:SerializedName("createdBy")
val creationTs: String,
#field:SerializedName("creationTs")
val createdBy: String,
#field:SerializedName("members")
val members: List<String>,
#field:SerializedName("favMembers")
val favMembers: List<String>
) {
// Does not show up in the response but set in post processing.
var isOneToOneChat: Boolean = false
var isChatBot: Boolean = false
}
Conversation Entity
#Entity(primaryKeys = ["msgId"],
foreignKeys = [
ForeignKey(entity = Channel::class,
parentColumns = arrayOf("channelId"),
childColumns = arrayOf("msgId"),
onUpdate = CASCADE,
onDelete = CASCADE
)
])
#TypeConverters(TypeConverters::class)
data class Conversation(
#field:SerializedName("msgId")
val msgId: String,
#field:SerializedName("employeeID")
val employeeID: String,
#field:SerializedName("channelId")
val channelId: String,
#field:SerializedName("channelName")
val channelName: String,
#field:SerializedName("sender")
val sender: String,
#field:SerializedName("sentAt")
val sentAt: String,
#field:SerializedName("senderName")
val senderName: String,
#field:SerializedName("status")
val status: String,
#field:SerializedName("msgType")
val msgType: String,
#field:SerializedName("type")
val panicType: String?,
#field:SerializedName("message")
val message: List<Message>,
#field:SerializedName("deliveredTo")
val delivered: List<Delivered>?,
#field:SerializedName("readBy")
val read: List<Read>?
) {
data class Message(
#field:SerializedName("txt")
val txt: String,
#field:SerializedName("lang")
val lang: String,
#field:SerializedName("trans")
val trans: String
)
data class Delivered(
#field:SerializedName("employeeID")
val employeeID: String,
#field:SerializedName("date")
val date: String
)
data class Read(
#field:SerializedName("employeeID")
val employeeID: String,
#field:SerializedName("date")
val date: String
)
// Does not show up in the response but set in post processing.
var isHeaderView: Boolean = false
}
Now as you can see Conversation belongs to a Channel. When user sees a list of channels, i need to display several attributes of last Conversation in the list item. My question is, is it enough if i just declare relation like above or should i contain Converstion object in Channel class? What are the other ways in which i can handle it? Because UI needs to get most recent conversation that happened along with time, status etc. in each item of the channel list when user scrolls. So there should not be any lag in UI because of this when i query.
And how can i have recent Converstaion object in Channel object?
I suggest create another class (not in DB, just for show in UI) like this:
data class LastConversationInChannel(
val channelId: String,
val channelName: String,
val creationTs: String,
val createdBy: String,
val msgId: String,
val employeeID: String,
val sender: String,
val sentAt: String,
val senderName: String
.
.
.
)
Get last Conversation in each Channel by this query:
SELECT Channel.*
,IFNULL(LastConversation.msgId,'') msgId
,IFNULL(LastConversation.sender,'') sender
,IFNULL(LastConversation.employeeID,'') employeeID
,IFNULL(LastConversation.sentAt,'') sentAt
,IFNULL(LastConversation.senderName,'') senderName
from Channel left join
(SELECT * from Conversation a
WHERE a.msgId IN ( SELECT b.msgId FROM Conversation AS b
WHERE a.channelId = b.channelId
ORDER BY b.sentAt DESC LIMIT 1 )) as LastConversation
on Channel.channelId = LastConversation.channelId
then use it in your dao like this:
#Query(" SELECT Channel.*\n" +
" ,IFNULL(LastConversation.msgId,'') msgId\n" +
" ,IFNULL(LastConversation.sender,'') sender\n" +
" ,IFNULL(LastConversation.employeeID,'') employeeID\n" +
" ,IFNULL(LastConversation.sentAt,'') sentAt\n" +
" ,IFNULL(LastConversation.senderName,'') senderName\n" +
" from Channel left join \n" +
" (SELECT * from Conversation a \n" +
" WHERE a.msgId IN ( SELECT b.msgId FROM Conversation AS b \n" +
" WHERE a.channelId = b.channelId \n" +
" ORDER BY b.sentAt DESC LIMIT 1 )) as LastConversation\n" +
" on Channel.channelId = LastConversation.channelId")
fun getLastConversationInChannel(): LiveData<List<LastConversationInChannel>>
is it enough if i just declare relation like above or should i contain Converstion object in Channel class?
You should not contain Conversation in Channel class, because Room will create some columns for it in Conversation table.
You can have an LastConversation which is an Conversation object inside the Chanel. You have to update this every time the lastConversation is updated by modify the table Chanel from Room layer. (Not take so much performance for update db). By implement sorting for the Chanel list (Comparable). Your UI update will be cool. And your logic from UI or ViewModel is simpler. I did it this way too.
I'm using room database, and I want to query table column as kotlin Tuple. Can anyone help me in same?
So I have a table named User which has multiple fields and Now I want to query 2 specific fields first_name and last_name and Kotlin Tuples(Pair)
#Entity(tableName = "user")
data class User(
var id: String,
var first_name: String,
var last_name: String
// some other fields
)
#Dao
interface UserDao {
#Query("SELECT first_name as first, last_name as second FROM user WHERE id = :id")
fun getUserName(id: String): Pair<String, String>
}
I'm getting a compiler error like below:
Not sure how to convert a Cursor to this method's return type (kotlin.Pair<java.lang.String,java.lang.String>)
This is how you can achieve it.
Create the class for Tuples. Include all the field which you want to get. In your case, the first and last names are the fields. This is concept is called Returning subsets of columns.
UserNameTuple
data class UserNameTuple(
#ColumnInfo(name = "first_name") val firstName: String?,
#ColumnInfo(name = "last_name") val lastName: String?
)
Design the query like this:
#Query("SELECT first_name, last_name FROM user WHERE id = :id")
fun getUserName(id: String): List<UserNameTuple>
You will get the data that satisfied the query condition with the tuple data.
This is how you can achieve it in your application.
You can also use secondary constructor in you User Class like below:
constructor(first_name: String, last_name: String) : this("", first_name, last_name)
Or since you have more field then you can use default value to avoid secondary constructor like below [I put "", you can change it according your requirements]:
data class User(
var id: String = "",
var first_name: String = "",
var last_name: String = ""
// some other fields
)
And modify getUserName like this
#Query("SELECT first_name, last_name FROM user WHERE id = :id")
fun getUserName(id: String): User