Define one-to-many relationship among Room entities for Chat model - android

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.

Related

Save complex JSON response in SQLite with Room

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).

Null Data Returned with Nested Relation in Room Database Android

Given that I have 3 entities, Order contains list of LineItem, each LineItem will associates with one Product by productId.
The problem that when I get data from OrderDao, it returns null for the product field, but in the lineItem field, it has data. While I can data with ProductWithLineItem.
Already tried a lot of work arounds but it does not work.
Here is my code for entities and dao
Entities
#Entity(tableName = DataConstant.ORDER_TABLE)
data class Order(
#PrimaryKey
#ColumnInfo(name = "orderId")
val id: String,
#ColumnInfo(name = "status")
var status: String
)
#Entity(tableName = DataConstant.LINE_ITEM_TABLE)
data class LineItem(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "lineItemId")
val id: Long,
#ColumnInfo(name = "productId")
val productId: String,
#ColumnInfo(name = "orderId")
val orderId: String,
#ColumnInfo(name = "quantity")
var quantity: Int,
#ColumnInfo(name = "subtotal")
var subtotal: Double
)
#Entity(tableName = DataConstant.PRODUCT_TABLE)
data class Product(
#PrimaryKey
#NonNull
#ColumnInfo(name = "productId")
val id: String,
#ColumnInfo(name = "name")
var name: String?,
#ColumnInfo(name = "description")
var description: String?,
#ColumnInfo(name = "price")
var price: Double?,
#ColumnInfo(name = "image")
var image: String?,
)
Relations POJOs
data class ProductAndLineItem(
#Embedded val lineItem: LineItem?,
#Relation(
parentColumn = "productId",
entityColumn = "productId"
)
val product: Product?
)
data class OrderWithLineItems(
#Embedded var order: Order,
#Relation(
parentColumn = "orderId",
entityColumn = "orderId",
entity = LineItem::class
)
val lineItemList: List<ProductAndLineItem>
)
Dao
#Dao
interface OrderDao {
#Transaction
#Query("SELECT * FROM `${DataConstant.ORDER_TABLE}` WHERE orderId = :id")
fun getById(id: String): Flow<OrderWithLineItems>
}
Result after running with Dao
Result after running query
Here is my code for entities and dao
You code appears to be fine, with the exception of returning a Flow, testing, using your code, but on the main thread using List (and no WHERE clause) i.e the Dao being :-
#Query("SELECT * FROM ${DataConstant.ORDER_TABLE}")
#Transaction
abstract fun getOrderWithLineItemsAndWithProduct(): List<OrderWithLineItems>
Results in :-
The data being loaded/tested using :-
db = TheDatabase.getInstance(this)
orderDao = db.getOrderDao()
orderDao.clearAll()
orderDao.insert(Product("product1","P1","desc1",10.01,"image1"))
orderDao.insert(Product("product2","P2","desc2",10.02,"image2"))
orderDao.insert(Product("product3","P3","desc3",10.03,"image3"))
orderDao.insert(Product("product4","P4","desc4",10.04,"image4"))
orderDao.insert(Product("","","",0.0,""))
val o1 = orderDao.insert(Order("Order1","initiaited"))
val o2 = orderDao.insert(Order("Order2","finalised")) // Empty aka no List Items
val o1l1 = orderDao.insert(LineItem(10,"product3","Order1",1,10.01))
val o1l2 = orderDao.insert(LineItem(20,"product4","Order1",2,20.08))
val o1l3 = orderDao.insert(LineItem(30,"","Order1",3,30.09))
val o1l4 = orderDao.insert(LineItem(40,"","x",1,10.01))
//val o1l3 = orderDao.insert(LineItem(30,"no such product id","Order1",10,0.0))
// exception whilst trying to extract if not commented out at test = ....
val TAG = "ORDERINFO"
val test = orderDao.getOrderWithLineItemsAndWithProduct()
for(owl: OrderWithLineItems in orderDao.getOrderWithLineItemsAndWithProduct()) {
Log.d(TAG,"Order is ${owl.order.id} status is ${owl.order.status}")
for(pal: ProductAndLineItem in owl.lineItemList) {
Log.d(TAG,"\tLine Item is ${pal.lineItem.id} " +
"for Order ${pal.lineItem.orderId} " +
"for ProductID ${pal.lineItem.productId} " +
"Quantity=${pal.lineItem.quantity} " +
"Product description is ${pal.product.description} Product Image is ${pal.product.image} Price is ${pal.product.price}")
}
}
As such I believe the issue might be that for some reason the Flow is detecting when the first query has completed but prior to the underlying queries.
That is when using #Relation the core objects (Order's) are extracted via the query and the core objects created then the related objects are extracted by a another query and used to build ALL the related objects as a List (unless just the one when it doesn't have to be a list). So prior to this underlying query the core object will have a null or an empty list for the underlying objects. Of course with a hierarchy of #Relations then this is replicated along/down the hierarchy.
I would suggest temporarily adding .allowMainThreadQueires to the databaseBuilder and using a List<OrderWithLineItems> or just a sole OrderWithLineItems. If using this then you get the Product(s) then the issue is with the Flow (which is what I suspect).

How to insert a set of nullable entites in Room?

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.

Using ignore on column in Room android

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
}

How to receive not duplicative rows using Room?

I have two related entities:
Station
#Entity(tableName = "stations")
data class Station(
#PrimaryKey
#ColumnInfo(name = "id")
val id: Long,
#ColumnInfo(name = "latitude")
val latitude: Double,
#ColumnInfo(name = "longitude")
val longitude: Double,
#ColumnInfo(name = "connectors")
val connectors: List<Connector>, // this field has a type converter
)
Connector
#Entity(
tableName = "connectors",
primaryKeys = ["station_id", "station_connector_id"]
)
class Connector(
#ColumnInfo(name = "station_connector_id")
val stationConnectorId: Int,
#ColumnInfo(name = "station_id")
val stationId: Long,
#ColumnInfo(name = "type")
val type: ConnectorType,
)
When I insert data, I fill up both entites and it seems kinda okay, but when I'm trying to receive stations with particular types of connectors it duplicates rows.
For example, I have an object
Station(
id = 100,
latitude = 56.565,
longitude = 34.565,
connectors = [
Connector(stationConnectorId=1, stationId=100, type=TYPE_2),
Connector(stationConnectorId=2, stationId=100, type=CHADEMO),
Connector(stationConnectorId=3, stationId=100, type=TYPE_1)
]
)
And if I want to filter stations only by one connector type I receive one row with this station(and it's right), but if I want to reset filters and look up for stations that can contain many connectors, I receive duplicates of this stations(in this example if I request for station with TYPE_1, TYPE_2 and CHADEMO connector types it will be three equal rows).
I'm using this query to request stations from my database:
SELECT * FROM simple_stations
INNER JOIN connectors ON simple_stations.id = connectors.station_id
WHERE connectors.type IN (:connectorTypesList)
I've tried to use DISTINCT in the query, ForeignKeys and Indexes in these Entities, but it was not working, so now I'm completely lost.
If you just want Stations then you have various options then your have various options.
is to use a GROUP BY clause such as GROUP BY simple_stations.id
However, the issue you may then encounter is that Station would be incomplete/unreliable as you have a List of Connectors and if you GROUP by Station then you will only get a single arbitrary Connector (there again that may depend upon you TypeConvertor).
to use DISTINCT you would have to only include the Station columns (similar problem as above).
I'd suggest that your schema is at fault by including the List of Connectors related to the Station you are duplicating data (aka it's not normalised).
rather if you removed
#ColumnInfo(name = "connectors")
val connectors: List<Connector>, // this field has a type converter
from the Station Entity the data itself would still be available for retrieval.
You may then wish to have a POJO that Embeds the Station and has a List of Connector's, perhaps one with and one without the Connector List have an #Relationship (with and you would get all connectors irrespective of the WHERE clause as that's how #Relationship works). Without and you could have the constructor get only the Connectors with the types you want.
Perhaps consider the following based upon your code:-
The Station Entity
#Entity(tableName = "stations")
data class Station(
#PrimaryKey
#ColumnInfo(name = "id")
val id: Long,
#ColumnInfo(name = "latitude")
val latitude: Double,
#ColumnInfo(name = "longitude")
val longitude: Double
/*
#ColumnInfo(name = "connectors")
val connectors: List<Connector> // this field has a type converter
NO NEED IMPLIED BY RELATIONSHIP
*/
)
The Connector Entity
#Entity(
tableName = "connectors",
primaryKeys = ["station_id", "station_connector_id"]
)
class Connector(
#ColumnInfo(name = "station_connector_id")
val stationConnectorId: Int,
#ColumnInfo(name = "station_id")
val stationId: Long,
#ColumnInfo(name = "type")
val type: String //<<<<< changed for convenience
)
The StationWithConnectors POJO NEW
class StationWithConnectors {
#Embedded
var station: Station? = null
var connectors: List<Connector> = emptyList()
constructor(allDao: AllDao, station: Station, connectorTypeList: List<String>) {
this.station = station
this.connectors = allDao.getConnectorsOfSpecifiedTypesByStationId(station.id,connectorTypeList)
}
}
note the embedded query to build the list of connectors of only the specfified types
The Dao used i.e. AllDao
#Dao
interface AllDao {
#Insert
fun insert(station: Station): Long
#Insert
fun insert(connector: Connector): Long
#Query("SELECT * FROM stations")
fun getAllStations(): List<Station>
#Query("SELECT * FROM stations WHERE stations.id = :stationId")
fun getStationById(stationId: Long): Station
// Gets the Connectors per Station of the requested Type (i.e. NOT ALL CONNECTORS necessarily)
#Query("SELECT * FROM connectors WHERE station_id = :stationId AND connectors.type IN( :connectorTypesList)")
fun getConnectorsOfSpecifiedTypesByStationId(stationId: Long, connectorTypesList: List<String>): List<Connector>
// Gets the Stations that have Connectors of the requested type DISTINCT used along with only the station columns
#Query("SELECT DISTINCT stations.id, stations.latitude, stations.longitude " +
"FROM stations INNER JOIN connectors ON connectors.station_id = stations.id " +
"WHERE connectors.type IN(:connectorTypesList)")
fun getStationsWithSpecificConnectorTypes(connectorTypesList: List<String>): List<Station>
}
The #Database TheDatabase
#Database(entities = [Connector::class,Station::class],version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
}
and finaly an Activity to test/demonstrate
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = Room.databaseBuilder(this,TheDatabase::class.java,"thedb.db")
.allowMainThreadQueries()
.build()
dao = db.getAllDao()
// Define some types
var type1 = "TYPE_1"
var type2 = "TYEP_2"
var type3 = "CHADEMO"
var type4 = "ANOTHER"
// Define stations with Connectors
var station1 = Station(100,56.565,34.565)
dao.insert(station1)
dao.insert(Connector(10,station1.id,type1))
dao.insert(Connector(20,station1.id,type4))
dao.insert(Connector(30,station1.id,type3))
dao.insert(Connector(40,station1.id ,type2))
var station2 = Station(200,33.333,22.222)
dao.insert(station2)
dao.insert(Connector(100,station2.id,type2))
dao.insert(Connector(110,station2.id,type4))
dao.insert(Connector(120,station2.id,type3))
// Define the search types
var listOfTypes = listOf(type1,type2) // Types to search for
// prepare the StationWithConnectors list
var allswcList: ArrayList<StationWithConnectors> = ArrayList()
// Get the stations with connectors of the required types
var stationsWithCertainTYpes = dao.getStationsWithSpecificConnectorTypes(listOfTypes)
// Build the StationWithCertainTypes POJOs
for(s: Station in stationsWithCertainTYpes) {
allswcList!!.add(StationWithConnectors(dao,s, listOfTypes))
}
var count = stationsWithCertainTYpes.size //<<<< just so breakpoint can be added
}
}
When run in debug mode then:-
StationWithCertainTypes gets both Stations (station 1 has type1 and type2, station2 has type2) as per :-
allswcList has the 2 StationWithConnectors built from the 2 Stations as per :-

Categories

Resources