Save complex JSON response in SQLite with Room - android

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

Related

How to define values in code block uses data class with room?

I fetch data from API and I would like to reuse it in data class like this (ItemStateRoom is data with String, Int etc.):
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
#ColumnInfo(name = "state")
#JsonAdapter(ItemStateAdapter::class)
var state: ItemStateRoom?
{
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
}
When I set state to null of course I have null, but program compiles well.
If it is like at the top error occurs:
error: 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).
I also tried to do like this:
#Ignore
#JsonAdapter(ItemStateAdapter::class)
#ColumnInfo(name = "state")
var state: ItemStateRoom?,
Then I have errors:
error: Cannot figure out how to read this field from a cursor.
.ItemStateRoom state;
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
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).
EDIT1:
Description: I make realm to room migration, realmobjects are downloaded as well and they are not null, but some of objects(data classes) I have changed to Room i.e. ItemStateRoom or ItemSubStatus. For sure it is not null, I download it from backend in postman it gives value for sure.
#TypeConverters(Converters::class)
#Entity(tableName = "itemsubstatus")
data class ItemSubStatusRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
#ColumnInfo(name = "title") val title: String = "",
#ColumnInfo(name = "description") val description: String? = null
)
TypeConverters:
class Converters {
private val gson = GsonBuilder().serializeNulls().create()
#TypeConverter
fun typeSubStatusToString(type: ItemSubStatusRoom?): String =
gson.toJson(type)
#TypeConverter
fun typeItemSubStatusFromString(value: String): ItemSubStatusRoom? =
gson.fromJson(value, ItemSubStatusRoom::class.java) as ItemSubStatusRoom?
}
My response data is simple:
class ItemsDataResponse (
#SerializedName("items")
val items: ArrayList<ItemRoom>,
val total: Int
)
in my ItemData it is also simple
#TypeConverters(Converters::class)
#Entity(tableName = "item")
data class ItemRoom #JvmOverloads constructor(#PrimaryKey(autoGenerate = true) var id: Int = 0,
[...]
#ColumnInfo(name = "sub_status")
var subStatus: ItemSubStatusRoom?,
``
When it is saying there is no "public constructor", it is saying that it does not know how to construct the ItemRoom object that does not include the state field/member.
So with state #Ignore'd you would need to have a constructor that doesn't expect the state as a parameter.
with a Data Class the definition within the parenthesises is the constructor as such.
e.g.
constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
in which case Room would always construct an ItemRoom where state is null.
However, I suspect that the above is not what you want. Rather that you want the state to be saved in the database. As such, as it is not a type that can be stored directly and hence the need for a TypeConverter (actually 2)
only String, integer (Int, Long, Byte, Boolean etc types), decimal (Float, Double etc types) or byte streams (ByteArray) can be stored directly
Then if you want the state field to be saved in the database, then you will need 2 TypeConverters.
One which will convert from the ItemStateRoom to one of the types that can be stored (so it is passed an ItemStateRoom and returns one of the types that can be stored directly).
The other will convert from the type stored to an ItemStateRoom object.
So assuming that you want to store the state you could have, something like :-
#TypeConverters(value = [Converters::class])
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
//#Ignore /*<<<<< if used a constructor is needed that doesn't require the state, as the state is not available */
/* if not used then state has to be converted to a type that room can store */
#ColumnInfo(name = "state")
#JsonAdapter(ItemStateAdapter::class)
var state: ItemStateRoom?
)
{
//constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
}
data class ItemStateRoom(
/* made up ItemStateRoom */
var roomNumber: Int,
var roomType: String
)
class ItemStateAdapter() {
/* whatever .... */
}
class Converters {
#TypeConverter
fun convertItemStateRoomToJSONString(itemStateRoom: ItemStateRoom): String = Gson().toJson(itemStateRoom)
#TypeConverter
fun convertFromJSONStringToItemStateRoom(jsonString: String): ItemStateRoom = Gson().fromJson(jsonString,ItemStateRoom::class.java)
}
note the commented out #Ignore and constructor (for if #Ignore'ing the state field).
how you implement, if at all, the #JsonAdapter, would be as you have it.
note the #TypeConverters (which you would omit if #Ignoreing the state field)
It might be preferable to include the #TypeConverters at the #Database level, where it has full scope. (see https://developer.android.com/reference/androidx/room/TypeConverters)
Example
Here's an example of storing the state (ItemStateRoom) that uses the code above, with a pretty standard #Database annotated class and an #Dao annotated interface :-
#Dao
interface ItemRoomDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(itemRoom: ItemRoom): Long
#Query("SELECT * FROM item")
fun getAllItems(): List<ItemRoom>
}
Two items are inserted and then extracted and written to the log using the following activity code :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: ItemRoomDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getItemRoomDao()
dao.insert(ItemRoom(type = "Bedroom", template = "The Template", title = "Bedroom1", state = ItemStateRoom(101,"King size - Shower - Bath - Kitchenette")))
dao.insert(ItemRoom(type = "Bedroom", template = "The Template", title = "Bedroom2", state = ItemStateRoom(102,"Queen size - Shower - Bath")))
for(i in dao.getAllItems()) {
Log.d("DBINFO","Item ID is ${i.id} Title is ${i.title} Room Number is ${i.state!!.roomNumber} Room Type is ${i.state!!.roomType}")
}
}
}
The result in the log:-
D/DBINFO: Item ID is 1 Title is Bedroom1 Room Number is 101 Room Type is King size - Shower - Bath - Kitchenette
D/DBINFO: Item ID is 2 Title is Bedroom2 Room Number is 102 Room Type is Queen size - Shower - Bath
The database via App Inspection :-
as can be seen the ItemStateRoom has been converted to (and from according to the output in the log) a JSON String.
Ignore the weatherforecast table, that was from another answer that was used for providing this answer.
Alternative Approach
Instead of converting the ItemState to a JSON representation (which is unwieldly from a database perspective) consider the potential advantages of instead embedding the ItemState. The difference in this approach is that the fields of the ItemState are each saved as individual columns.
e.g.
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
//#Ignore /*<<<<< if used a constructor is needed that doesn't require the state, as the state is not available */
/* if not used then state has to be converted to a type that room can store */
//#ColumnInfo(name = "state")
//#JsonAdapter(ItemStateAdapter::class)
//var state: ItemStateRoom?
#Embedded
var itemStateRoom: ItemStateRoom
)
{
//constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
/*
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
*/
}
Thus to update, at least from the database aspect, you are freed from trying to manipulate a representation of the data. You can update the actual data directly.
Using the above, with the example code (changed to use itemState instead of state) then the database looks like:-
You say
But to reassign values dynamically I have to use #JsonAdapter.
The JsonAdapter is NOT going to magically update the data in the database, to update the data you will have to use a function in an interface (or an abstract class) that is annotated with #Dao which will either be annotated with #Update (convenience) or #Query (with appropriate UPDATE SQL).

how to add two tables in one model class (Room) android studio

The problem is: I am getting news from API and cache them in an entity(articles_table) with (Articles class), so I want to add another entity(bookmark_table) with the same (Articles) model class.
I want inside Articles model class:
First articles_table (this will represent the home cache news)
second articles_bookmark_table (this will represent user bookmarks)
is this possible? to have two tables in one class? or there is another way that does the same thing?
#Entity(tableName = "articles_table")
data class Articles(
#PrimaryKey(autoGenerate = true)
val id: Int,
val author: String,
val date: String,
val img: String,
val source: String,
val title: String,
val url: String,
val interest:String
)
is this possible? to have two tables in one class?
No, an #Entity defines a class as a table as per
Marks a class as an entity. This class will have a mapping SQLite table in the database. https://developer.android.com/reference/androidx/room/Entity
You could embed the Article Class into another #Entity class and have what is effectively a second, nearly identically structured, table.
e.g. :-
#Entity(tableName = "articles_bookmark_table", primaryKeys = ["id"])
data class BookMarkArticles(
#Embedded
val articles: Articles
)
note that when Embedding the Primary Key is not included, hence the need to define it.
I would suggest that this is a precarious way to go as it may result in confusion over ambiguous columns as the column names would be identical.
However, I'd suggest that rather than 2 basically identical tables, that you simply add an indicator (Boolean) that determines whether or not it is a bookmark row or not. e.g. :-
#Entity(tableName = "articles_table")
data class Articles(
#PrimaryKey //<<<<<<<<<< autoGenerate removed
val id: Int? = null, //<<<<<<<<<< defaults to null so if not provided auto generates id
val author: String,
val date: String,
val img: String,
val source: String,
val title: String,
val url: String,
val interest:String,
val bookmark:Boolean //<<<<<<<<<< ADDED to distinguish bookmark row or not
)
using the 2 tables with the embedded introduces a nuance when inserting/extracting, as the following demo.
The demo uses slightly modified Articles and BookMarkArticles and the 2nd Articles as ArticlesV2 to show both ways together.
So Articles is :-
#Entity(tableName = "articles_table")
data class Articles(
#PrimaryKey
val id: Int? = null,
val author: String,
val date: String,
val img: String,
val source: String,
val title: String,
val url: String,
val interest:String
)
-i.e. id is allowed to be null and thus when not provided the id will be automatically generated.
BookMarkArticles (Embeds Articles) is :-
#Entity(tableName = "articles_bookmark_table", primaryKeys = ["id"])
data class BookMarkArticles(
#Embedded
val articles: Articles
)
ArticlesV2 (the two types in a single table) is :-
#Entity(tableName = "articles_v2_table")
data class ArticlesV2(
#PrimaryKey
val id: Int? = null,
val author: String,
val date: String,
val img: String,
val source: String,
val title: String,
val url: String,
val interest:String,
val bookmark:Boolean
)
Dao's are :-
#Insert
abstract fun insert(articles: Articles): Long
#Insert
abstract fun insert(bookMarkArticles: BookMarkArticles): Long
#Insert
abstract fun insert(articlesV2: ArticlesV2): Long
#Query("SELECT * FROM articles_table")
abstract fun getAllArticles(): List<Articles>
#Query("SELECT * FROM articles_bookmark_table")
abstract fun getAllBookmarkedArticles(): List<BookMarkArticles>
#Query("SELECT * FROM articles_v2_table WHERE bookmark=:bookmarked")
abstract fun getAllArticleV2s(bookmarked: Boolean): List<ArticlesV2>
In an Activity is the following code :-
db = TheDatabase.getInstance(this)
dao = db.getDao()
dao.insert(Articles(author = "Fred",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest"))
dao.insert(Articles(author = "Bert",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest"))
dao.insert(BookMarkArticles(Articles(author = "Fred",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest")))
dao.insert(BookMarkArticles(Articles(author = "Bert",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest")))
dao.insert(ArticlesV2(author = "Fred",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest",bookmark = false))
dao.insert(ArticlesV2(author = "Bert",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest",bookmark = false))
dao.insert(ArticlesV2(author = "Fred",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest",bookmark = true))
dao.insert(ArticlesV2(author = "Bert",date = "2021-01-01",img = "MyImage", title = "MyTitle", url = "MyUrl",source = "MySource",interest = "MyInterest",bookmark = true))
for(a: Articles in dao.getAllArticles()) {
Log.d("ARTICLEINFO","(not bookmark) Author is ${a.author} ID is ${a.id} etc")
}
for(b: BookMarkArticles in dao.getAllBookmarkedArticles()) {
Log.d("ARTICLEINFO","(bookmark) Author is ${b.articles.author} ID is ${b.articles.id} etc")
}
for(a: ArticlesV2 in dao.getAllArticleV2s(false)) {
Log.d("ARTICLEV2INFO","(not bookmark) Author is ${a.author} ID is ${a.id} etc BOOKMARK Flag is ${a.bookmark}")
}
for(a: ArticlesV2 in dao.getAllArticleV2s(true)) {
Log.d("ARTICLEV2INFO","(bookmark) Author is ${a.author} ID is ${a.id} etc BOOKMARK Flag is ${a.bookmark}")
}
Note the nuance (nusiance) that a BookMarkArticles is constructed via an Articles object and that when extracting a BookMarkArticles that it has an embedded Articles object.
The result in the log :-
2021-09-01 16:32:09.844 D/ARTICLEINFO: (not bookmark) Author is Fred ID is 1 etc
2021-09-01 16:32:09.845 D/ARTICLEINFO: (not bookmark) Author is Bert ID is 2 etc
2021-09-01 16:32:09.847 D/ARTICLEINFO: (bookmark) Author is Fred ID is 1 etc
2021-09-01 16:32:09.847 D/ARTICLEINFO: (bookmark) Author is Bert ID is 2 etc
2021-09-01 16:32:09.849 D/ARTICLEV2INFO: (not bookmark) Author is Fred ID is 1 etc BOOKMARK Flag is false
2021-09-01 16:32:09.849 D/ARTICLEV2INFO: (not bookmark) Author is Bert ID is 2 etc BOOKMARK Flag is false
2021-09-01 16:32:09.851 D/ARTICLEV2INFO: (bookmark) Author is Fred ID is 3 etc BOOKMARK Flag is true
2021-09-01 16:32:09.851 D/ARTICLEV2INFO: (bookmark) Author is Bert ID is 4 etc BOOKMARK Flag is true

Android Room doesn't allow me to do the one-to-N relationship multiple times

I have implemented a DB where I have two one-to-many relationships but it would seem that room does not allow it. Is that so?
The entities are:
#Entity(tableName = "arete_sheet")
data class EAreteSheet(
#PrimaryKey val id: Long,
#ColumnInfo(name = "sheet") val form: Sheets,
#ColumnInfo(name = "version") val version: Int,
)
#Entity(tableName = "arete_sheet_paragraph")
data class EAreteSheetParagraph(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_id") val sheet: Long,
#ColumnInfo(name = "name") val name: String
)
#Entity(tableName = "arete_sheet_form")
data class EAreteSheetForm(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_paragraph_id") val paragraph: Long,
#ColumnInfo(name = "fieldType") val fieldType: FieldType,
#ColumnInfo(name = "cell") val cell: String,
#ColumnInfo(name = "label") val label: String
)
To solve the schema I have implemented these join classes:
data class EAreteSheetWithParagraph(
#Embedded val sheet: EAreteSheet,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_id"
)
val paragraph: List<EAreteSheetParagraphWithForm>
)
data class EAreteSheetParagraphWithForm(
#Embedded val paragraph: EAreteSheetParagraph,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_paragraph_id"
)
val forms: List<EAreteSheetForm>
)
This is the DAO implementation:
#Transaction
#Query("SELECT * FROM arete_sheet")
suspend fun getSheetWithParagraphsAndForms(): List<EAreteSheetWithParagraph>
This is the mistake he gives me in the building phase:
app/build/generated/source/kapt/debug/it/ximplia/agri2000/model/db/dao/AreteSheetDAO_Impl.java:203: error: constructor EAreteSheetWithParagraph in class EAreteSheetWithParagraph cannot be applied to given types;
_item = new EAreteSheetWithParagraph();
^
required: EAreteSheet,List<EAreteSheetParagraphWithForm>
found: no arguments
reason: actual and formal argument lists differ in length
app/build/generated/source/kapt/debug/it/ximplia/agri2000/model/db/dao/AreteSheetDAO_Impl.java:204: error: sheet has private access in EAreteSheetWithParagraph
_item.sheet = _tmpSheet;
I think that Room does not allow to resolve dependencies in cascade but I would like to know if someone was successful or if I made a mistake before changing the code.
In EAreteSheetWithParagraph you are specifying a list of EAreteSheetParagraphWithForm's
as per :-
val paragraph: List<EAreteSheetParagraphWithForm>
The #Relation will try to ascertain the columns as per the EAreteSheetParagraphWithForm form class, which is not an Entity. You should change the #Relation to specify the appropriate entity (i.e. the EAreteSheetParagraph) using the entity parameter.
So EAreteSheetWithParagraph should be something like :-
data class EAreteSheetWithParagraph(
#Embedded val sheet: EAreteSheet,
#Relation(
entity = EAreteSheetParagraph::class,
parentColumn = "id",
entityColumn = "arete_sheet_id"
)
val paragraph: List<EAreteSheetParagraphWithForm>
)
However, I don't believe that you are getting that far, as the messages appear to be complaining about the sheet variable which has a type of Sheets (and that the variable is private).
Without adding the entity= parameter then the compile, if it reached that stage, would fail with :-
> Task :app:kaptDebugKotlin FAILED
error: The class must be either #Entity or #DatabaseView. - a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm
E:\AndroidStudioApps\SO68953488KotlinRoomMany1N\app\build\tmp\kapt3\stubs\debug\a\a\so68953488kotlinroommany1_n\EAreteSheetWithParagraph.java:12: error: Cannot find the child entity column `arete_sheet_id` in a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm. Options:
private final java.util.List<a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm> paragraph = null;
^
The class must be either #Entity or #DatabaseView. - a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm
I think that Room does not allow to resolve dependencies in cascade but I would like to know if someone was successful or if I made a mistake before changing the code.
As indicated above, I believe the issue is mistakes so :-
Proof of concept
Based upon your code the following shows that nested/multiple 1-n's do work:-
Using your code BUT with the following changes (to avoid issues and simplify) :-
#Entity(tableName = "arete_sheet_form")
data class EAreteSheetForm(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_paragraph_id") val paragraph: Long,
#ColumnInfo(name = "fieldType") val fieldType: /* FieldType */ String, // changed for convenience/brevity
#ColumnInfo(name = "cell") val cell: String,
#ColumnInfo(name = "label") val label: String
)
fieldType' stype changed from FieldType to String so no need for extra class and type converters.
and
#Entity(tableName = "arete_sheet")
data class EAreteSheet(
#PrimaryKey val id: Long,
#ColumnInfo(name = "sheet") val form: /*Sheets*/ String, // changed for convenience/brevity
#ColumnInfo(name = "version") val version: Int,
)
Sheets type substituted with String
EAreteSheetWithParagraph as above
EAreteSheetParagraphWithForm changed to
data class EAreteSheetParagraphWithForm(
#Embedded val paragraph: EAreteSheetParagraph,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_paragraph_id",
entity = EAreteSheetForm::class
)
val forms: List<EAreteSheetForm>
)
i.e. the entity parameter has been added according to my preference to always code the entity parameter.
AreteSheetDao used :-
#Dao
abstract class AreteSheetDAO {
#Insert
abstract fun insert(eAreteSheet: EAreteSheet): Long
#Insert
abstract fun insert(eAreteSheetParagraph: EAreteSheetParagraph): Long
#Insert
abstract fun insert(eAreteSheetForm: EAreteSheetForm): Long
#Transaction
#Query("SELECT * FROM arete_sheet")
abstract fun getSheetWithParagraphsAndForms(): List<EAreteSheetWithParagraph>
}
Code in an Activity :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AreteSheetDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getDao()
var s1 = dao.insert(EAreteSheet(10,"Sheet1",1))
var s2 = dao.insert(EAreteSheet(20,"Sheet2",1))
var p1 = dao.insert(EAreteSheetParagraph(100,s1,"Para1 (Sheet1)"))
var p2 = dao.insert(EAreteSheetParagraph(101,s1,"Para2 (Sheet1)"))
var p3 = dao.insert(EAreteSheetParagraph(201,s2,"Para3 (Sheet2)"))
var p4 = dao.insert(EAreteSheetParagraph(202,s2,"Para4 (Sheet2)"))
var f1 = dao.insert(EAreteSheetForm(1000,p1,"typex","cellx","Form1"))
var f2 = dao.insert(EAreteSheetForm(1001,p1,"typex","cellx","Form2"))
var f3 = dao.insert(EAreteSheetForm(1002,p1,"typex","cellx","Form3"))
var f4 = dao.insert(EAreteSheetForm(1010,p2,"typex","cellx","Form4"))
var f5 = dao.insert(EAreteSheetForm(1011,p2,"typex","cellx","Form5"))
var f6 = dao.insert(EAreteSheetForm(1020,p3,"typex","cellx","Form6"))
val TAG = "ARETEINFO"
for(sw: EAreteSheetWithParagraph in dao.getSheetWithParagraphsAndForms()) {
Log.d(TAG,"Sheet ID is ${sw.sheet.id} Form is ${sw.sheet.form} Version is ${sw.sheet.version}" )
for(pf: EAreteSheetParagraphWithForm in sw.paragraph) {
Log.d(TAG,"\tPara is ${pf.paragraph.name} etc")
for(f: EAreteSheetForm in pf.forms) {
Log.d(TAG,"\t\tForm is ${f.label}")
}
}
}
}
}
As can be seen, some data is loaded, 2 Sheets, each with 2 paragraphs and then 4 forms distributed unevenly (3 to para1, 2 to para2, 1 to para3, 0 to para4).
The data is then extracted (as array of EAreteSheetWithParagraph), the array traversed (traversing the underlying arrays of paragraphs and forms within paragraphs) and output to the log the Result being :-
D/ARETEINFO: Sheet ID is 10 Form is Sheet1 Version is 1
D/ARETEINFO: Para is Para1 (Sheet1) etc
D/ARETEINFO: Form is Form1
D/ARETEINFO: Form is Form2
D/ARETEINFO: Form is Form3
D/ARETEINFO: Para is Para2 (Sheet1) etc
D/ARETEINFO: Form is Form4
D/ARETEINFO: Form is Form5
D/ARETEINFO: Sheet ID is 20 Form is Sheet2 Version is 1
D/ARETEINFO: Para is Para3 (Sheet2) etc
D/ARETEINFO: Form is Form6
D/ARETEINFO: Para is Para4 (Sheet2) etc

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.

Serialized Data Class combined with built-in modifications

I am working on updating the parsing of an API response that uses a Serialized Data Class to parse the JSON response. The serialization works perfectly fine right now, but the new data that I'm attempting to parse into data class is not fully reliant on data in the json. Here is what I mean by that:
The data class is Career, and the new data I need to parse is a set of skills and each have a rating. The json data is very simple and contains the skills as such:
{
// other career data
...
"mathematics_skill": 8,
"critical_thinking_skill": 6
... // the remaining skills
}
Using straight serialization, I would only be able to store the data as such:
data class Career(
// Other career data
#serializableName("mathematic_skill") val mathSkill: Int,
#serializableName("critical_thinking_skill") val mathSkill: Int,
// remaining skills
)
However, I would like to store all skills in an array variable of a custom skills class that not only contains the rating, but also the name of the skill and a color. Basically, when I access the skills data of a career, I would like to access it like such:
val careerMathSkill = career.skills[0]
val mathRating = careerMathSkill.rating
val mathColor = careerMathSkill.color
Is it possible to use the serialized data from the data class to add non-serialized data to the same data class? (Sorry for the weird wording, not sure how else to explain it)
EDIT: Here is what I have:
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
)
}
Here is what I am hoping to do in a way
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
// skills array that will need to be filled out based on the data I got in the json
var skills: List<Skill>
)
}
EDIT: The suggested solution
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
#SerializedName("math_skill") val mathSkill: Int
#SerializedName("other_skill") val mathSkill: Int
) {
var skills: List<Skill> = {
val mathSkill = Skill(name: "Math", rating: mathSkill, color: /**some color*/)
val otherSkill = Skill(name: "Other", rating: otherSkill, color: /**some color*/)
return listOf(mathSkill, otherSkill)
}
}
}
Yes, you can create a custom JsonDeserializer to modify how the JSON is parsed.
Here is a basic example of what that would look like.
class CareerDeserializer : JsonDeserializer<Career> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Career {
val obj = json.asJsonObject
// standard career data
val id = obj.get("id")?.asString
val name = obj.get("name").asString
// making a Skill object
val skill = Skill(
obj.get("mathematic_skill").asInt,
obj.get("critical_thinking_skill").asInt,
obj.get("swimming_skill").asInt
// etc
)
return Career(id, name, skill)
}
}
And make sure to register that within your GsonBuilder.
val gson = GsonBuilder()
.registerTypeAdapter(Career::class.java, CareerDeserializer())
.create()
Note, you'll also have to create a JsonSerializer if you want to go the other way too.
Edit:
However, if you're just looking to change the syntax of how you're accessing that data, you can do something like this.
data class Career(
// Other career data
val mathSkill: Int,
val thinkSkill: Int
// remaining skills
) {
val skills: List<Int>
get() = listOf(mathSkill, thinkSkill)
}
This would give you a skills list back whenever you needed it, and it would be created when you accessed it, so you won't have to worry about the data being out of sync. This would allow you to access your data as such.
career.skills[0] // get the math skill.
And you can take this another step further by adding a get operator to your Career class.
data class Career(
// Other career data
val mathSkill: Int,
val thinkSkill: Int
// remaining skills
) {
...
operator fun get(pos: Int) = skills[pos]
}
Now, you can simply do
career[0] // get the math skill.
Warning, this is dangerous because you're accessing an Array so you could get OutOfBoundsExceptions. Use constants to help you out.
Edit 2:
val skills = {
listOf(Skill("Math", mathSkill, /** some color */ ),
Skill("Other", otherSkill, /** some color */ ))
}

Categories

Resources