How to create Entity and data classes by ROOM in android?
I have JSON structure:
data class ListResponse(val item: ListItem)
data class ListItem(
#SerializedName("id")
val id: List<CheckUnCheckItem>
)
data class CheckUnCheckItem(
#SerializedName("check")
val check: CheckItem,
#SerializedName("unCheck")
val UnCheck: UnCheckItem
)
data class CheckItem(
#SerializedName("url")
val url: String,
#SerializedName("text")
val text: String,
#SerializedName("color")
val color: String
)
data class UnCheckItem(
#SerializedName("url")
val urlUnCheck: String,
#SerializedName("text")
val textUnCheck: String,
#SerializedName("color")
val colorUnCheck: String
)
But How can I create such ROOM Entity?
Do I need to use #TypeConverter?
#Entity(tableName = TABLE_NAME)
data class ListEntity(
#PrimaryKey #SerializedName("id")
val id: CheckUnCheckItem,
#SerializedName("check")
val check: CheckItem,
#SerializedName("unCheck")
val unCheck: UnCheckItem,
#SerializedName("url")
val url: String,
#SerializedName("text")
val text: String,
#SerializedName("size")
val size: String
){
companion object{
const val TABLE_NAME = "db_table"
}
class RoomTypeConverters{
#TypeConverter
fun convertCheckItemListToJSONString(checkList: CheckItem): String = Gson().toJson(checkList)
#TypeConverter
fun convertJSONStringToCheckItemList(jsonString: String): CheckItem = Gson().fromJson(jsonString,CheckItem::class.java)
}
}
is my data and entity classes are correct?
Do I need class witch extends RoomDatabase?
Or better I need to separate db and create for check and uncheck another db?
Or better I need to separate db and create for check and uncheck another db?
As database implies it is able to store data not just one but many. As such a single database is all that would be required. SQLite is a relational database and is designed to store related data. Related data is typically stored in multiple tables. So again a single database will very likely be sufficient.
Do I need to use #TypeConverter?
You never actually need Type Converters. However, for any Object, other than those directly handled (e.g. String, Int, Long, Double, Float, ByteArray) then you either need to break these down into such handled objects or have a types converter that will convert the object to and from such an object.
For example, based upon your #Entity annotated ListEntity class then:-
field id would need a TypeConverter as the type CheckUnCheckItem is not an object type that can be directly handled by Room. So you would need two TypeConverters that could convert from a CheckUncheckItem to and from a type that can be handled by Room.
fields check and uncheck would need two TypeConverters (and it looks as though the Type Converters you have coded will handle the conversion).
fields url,text and size, as they are all String types do not need Type Converters as Room handles strings.
Room has to know about the Type Converters. So you need an #TypeConverters annotation. It's placement defines the scope. Using the annotation to preced the #Database annotation has the most far reaching scope.
Do I need class witch extends RoomDatabase?
Yes. However it has to be an abstract class and should have an abstract function to retrieve an instance of each #Dao annotated interface (or abstract class, in which case the functions have to be abstract, there is no need for an abstract class with Kotlin as functions in an interface can have bodies)).
This class should be annotated with the #Database annotation, the entities parameter of the annotation should include the list of classes for each each table (#Entity annotated class). e.g.
#TypeConverters(value = [ListEntity.RoomTypeConverters::class])
#Database(entities = [ListEntity::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase(){
}
However, using the above along with your classes results in a build error as per:-
ListEntity.java:11: error: Cannot figure out how to save this field into database. You can consider adding a type converter for it. private final a.a.so74708202kotlinroomentitydesign.CheckUnCheckItem id = null;
as explained CheckUnCheckItem cannot be handled by Room.
Amending the RoomTypeConverters class to be:-
class RoomTypeConverters{
#TypeConverter
fun convertItemListToJSONString(invoiceList: Item): String = Gson().toJson(invoiceList)
#TypeConverter
fun convertJSONStringToItemList(jsonString: String): Item = Gson().fromJson(jsonString,Item::class.java)
#TypeConverter
fun convertCheckUnCheckItemToJSONString(cuc: CheckUnCheckItem): String = Gson().toJson(cuc)
#TypeConverter
fun convertJSONStringToCheckUnCheckItem(jsonString: String): CheckUnCheckItem = Gson().fromJson(jsonString,CheckUnCheckItem::class.java)
}
Resolves the build issue and in theory you have a potentially usable database.
However, you obviously need code to access the database. As such you would very likely want to have. as previously mentioned, an #Dao annotated interface e.g
#Dao
interface TheDAOs {
#Insert
fun insert(listEntity: ListEntity): Long
#Query("SELECT * FROM ${TABLE_NAME}")
fun getAll(): List<ListEntity>
}
This will suffice to allow rows to be inserted into the database and for all the rows to be extracted from the database into a List<ListEntity).
From the built database you need to get an instance of the TheDAOs and thus the #Database annotated class could then be
:-
#TypeConverters(value = [ListEntity.RoomTypeConverters::class])
#Database(entities = [ListEntity::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase(){
abstract fun getTheDAOsInstance(): TheDAOs
}
To demonstrate actual use of the above then consider the following code in an activity:-
class MainActivity : AppCompatActivity() {
lateinit var roomDBInstance: TheDatabase
lateinit var theDAOs: TheDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
roomDBInstance = Room.databaseBuilder(this,TheDatabase::class.java,"The_database_name.db")
.allowMainThreadQueries() /* NOTE ADDED FOR CONVENIENCE AND BREVITY */
.build()
/* Note the database itself does not yet exist, it's creation is delayed until an attempt is made to access it. So:- */
theDAOs = roomDBInstance.getTheDAOsInstance() /* Still the database is not created/accessed */
showData(theDAOs.getAll()) /* No data has been added BUT the database will now exist */
theDAOs.insert(
ListEntity(
id = CheckUnCheckItem(
check = Item (
url ="URL001",
text = "TEXT001",
color = "RED"
),
unCheck = Item(
url ="URL002",
text = "TEXT002",
color = "BLUE"
)
),
check = Item(url = "URL003", text ="TEXT003", color ="WHITE"),
unCheck = Item(url = "URL004", text = "TEXT004", color = "BLACK"),
url = "URL005", text = "TEXT005", size = "BIG"
)
)
showData(theDAOs.getAll())
}
fun showData(listEntities: List<ListEntity>) {
for (li in listEntities) {
Log.d(
"DBINFO",
"id is $li.id.check.url${li.id.check.text}.... " +
"\n\tcheck is ${li.check.url} .... " +
"\n\tuncheck is ${li.unCheck.url} ...." +
"\n\turl is ${li.url} text is ${li.text} size is ${li.size}"
)
}
}
}
The output to the log being:-
D/DBINFO: id is ListEntity(id=CheckUnCheckItem(check=Item(url=URL001, text=TEXT001, color=RED), unCheck=Item(url=URL002, text=TEXT002, color=BLUE)), check=Item(url=URL003, text=TEXT003, color=WHITE), unCheck=Item(url=URL004, text=TEXT004, color=BLACK), url=URL005, text=TEXT005, size=BIG).id.check.urlTEXT001....
check is URL003 ....
uncheck is URL004 ....
url is URL005 text is TEXT005 size is BIG
The Database via App Inspection being"-
So finally
is my data and entity classes are correct?
From a database aspect yes, they work after a few amendments. However, I suspect that your classes are probably not what you intended.
An Alternative Approach
If this were to be approached from a database perspective and normalised and without bloat and without the need for type converters then consider the following:-
The embedded Item's (uncheck and check) are basically repetition, so could probably be a table (related to the db_table). Hence 2 tables. One for the ListEntity (Alternative) and another for the Items (AlternativeItem) so the 2 #Entity annotated classes could be:-
/* Alternative Approach */
#Entity(
/* Foreign Keys NOT REQUIRED, they enforce Referential Integrity */
foreignKeys = [
ForeignKey(
entity = AlternativeItem::class,
parentColumns = ["alternativeItemId"],
childColumns = ["unCheckIdMap"]
/* OPTIONAL within a Foreign Key, they help automatically maintain Referential Integrity*/,
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = AlternativeItem::class,
parentColumns = ["alternativeItemId"],
childColumns = ["checkIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Alternative(
#PrimaryKey
val id: Long?=null,
#ColumnInfo(index = true)
val unCheckIdMap: Long, /* map to the id of the related Item (AlternativeItem) for the uncheck */
#ColumnInfo(index = true)
val checkIdMap: Long, /* map to the id of the related Item (AlternativeItem) for the uncheck */
val url: String,
val text: String,
val size: String
)
#Entity
data class AlternativeItem(
#PrimaryKey
val alternativeItemId: Long?=null,
val alternativeItemUrl: String,
val alternativeItemText: String,
val alternativeItemColor: String
)
As you would typically want the Alternative along with it's related AlternativeItems then a POJO that caters for the togetherness :-
data class AlternativeWithUncheckAndCheck(
#Embedded
val alternative: Alternative,
#Relation(entity = AlternativeItem::class, parentColumn = "unCheckIdMap", entityColumn = "alternativeItemId")
val unCheck: AlternativeItem,
#Relation(entity = AlternativeItem::class, parentColumn = "checkIdMap", entityColumn = "alternativeItemId")
val check: AlternativeItem
)
There would be a need for some extra functions in the #Dao annotated interface, so :-
#Insert
fun insert(alternative: Alternative): Long
#Insert
fun insert(alternativeItem: AlternativeItem): Long
#Transaction
#Query("")
fun insertAlternativeAndUncheckAndCheck(alternative: Alternative, uncheck: AlternativeItem, check: AlternativeItem): Long {
var uncheckId = insert(uncheck)
var checkId = insert(check)
return insert(Alternative(null,url = alternative.url, text = alternative.text, size = alternative.size, unCheckIdMap = uncheckId, checkIdMap = checkId ))
}
#Transaction
#Query("SELECT * FROM alternative")
fun getAllAlternativesWithRelatedUnCheckAndCheck(): List<AlternativeWithUncheckAndCheck>
note that the insertAlternativeAndUncheckAndCheck does what it says (note that it is overly simple and could need some enhancements to expand upon the principle)
To demonstrate this, all that is then required is to add the new entities to the entities parameter and to then add some code to the activity.
The amended #Database annotation:-
#Database(entities = [ListEntity::class, /* for the alternative approach */ Alternative::class, AlternativeItem::class], exportSchema = false, version = 1)
The activity code (that caters for both approaches in a similar/equivalanet way of storing and retrieving the data) :-
class MainActivity : AppCompatActivity() {
lateinit var roomDBInstance: TheDatabase
lateinit var theDAOs: TheDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
roomDBInstance = Room.databaseBuilder(this,TheDatabase::class.java,"The_database_name.db")
.allowMainThreadQueries() /* NOTE ADDED FOR CONVENIENCE AND BREVITY */
.build()
/* Note the database itself does not yet exist, it's creation is delayed until an attempt is made to access it. So:- */
theDAOs = roomDBInstance.getTheDAOsInstance() /* Still the database is not created/accessed */
showData(theDAOs.getAll()) /* No data has been added BUT the database will now exist */
theDAOs.insert(
ListEntity(
id = CheckUnCheckItem(
check = Item (
url ="URL001",
text = "TEXT001",
color = "RED"
),
unCheck = Item(
url ="URL002",
text = "TEXT002",
color = "BLUE"
)
),
check = Item(url = "URL003", text ="TEXT003", color ="WHITE"),
unCheck = Item(url = "URL004", text = "TEXT004", color = "BLACK"),
url = "URL005", text = "TEXT005", size = "BIG"
)
)
showData(theDAOs.getAll())
/* Alternative equivalent */
theDAOs.insertAlternativeAndUncheckAndCheck(
Alternative(url = "URL005", size = "BIG", text = "TEXT005", checkIdMap = -1, unCheckIdMap = -1),
check = AlternativeItem(alternativeItemUrl = "URL001", alternativeItemText = "TEXT001", alternativeItemColor = "RED"),
uncheck = AlternativeItem(alternativeItemUrl = "URL002", alternativeItemText = "TEXT002", alternativeItemColor = "BLUE" )
)
showAlternativeData(theDAOs.getAllAlternativesWithRelatedUnCheckAndCheck())
}
fun showData(listEntities: List<ListEntity>) {
for (li in listEntities) {
Log.d(
"DBINFO",
"id is $li.id.check.url${li.id.check.text}.... " +
"\n\tcheck is ${li.check.url} .... " +
"\n\tuncheck is ${li.unCheck.url} ...." +
"\n\turl is ${li.url} text is ${li.text} size is ${li.size}"
)
}
}
fun showAlternativeData(listAlternatives: List<AlternativeWithUncheckAndCheck>) {
for (la in listAlternatives) {
Log.d("DBALTINFO",
"id is ${la.alternative.id} URL is ${la.alternative.url} TEXT is ${la.alternative.text} SIZE is ${la.alternative.size} " +
"\n\t UNCHECK id is ${la.unCheck.alternativeItemId} url is ${la.unCheck.alternativeItemUrl} text is ${la.unCheck.alternativeItemText} color is ${la.unCheck.alternativeItemColor}" +
"\n\t CHECK id is ${la.check.alternativeItemId} url is ${la.check.alternativeItemUrl} text is ${la.check.alternativeItemText} color is ${la.check.alternativeItemColor}")
}
}
}
Note that the Alternative code is probably more along the lines of what you probably want according to the interpretation of the shown JSON.
When run then the result is now:-
D/DBINFO: id is ListEntity(id=CheckUnCheckItem(check=Item(url=URL001, text=TEXT001, color=RED), unCheck=Item(url=URL002, text=TEXT002, color=BLUE)), check=Item(url=URL003, text=TEXT003, color=WHITE), unCheck=Item(url=URL004, text=TEXT004, color=BLACK), url=URL005, text=TEXT005, size=BIG).id.check.urlTEXT001....
check is URL003 ....
uncheck is URL004 ....
url is URL005 text is TEXT005 size is BIG
D/DBALTINFO: id is 1 URL is URL005 TEXT is TEXT005 SIZE is BIG
UNCHECK id is 1 url is URL002 text is TEXT002 color is BLUE
CHECK id is 2 url is URL001 text is TEXT001 color is RED
it is suspected that BLACK/WHITE or RED/BLUE is superfluous in your interpretationof the JSON to data classes (and hence excluded in the alternative).
The database, via App Inspection (in regards to the alternative approach) is:-
and :-
i.e. only the actual data is store the BLOAT (field/type descriptions, separators, enclosing data) is not stored thus
the database will hold more data in less space.
the handling of the data will thus be more efficient (e.g. a buffer can hold more actual data instead of BLOAT).
querying the data such as for example searching for all BLUE's is directly a search for that, whilst with converted data you may have issues distinguishing between BLOAT and actual data
However, the negative, is that more code and thought is required.
Note this answer is intended to deal with the basic principles and is most certainly not fully comprehensive.
How do I ignore the primary key when I insert some entity?
Room Entity has to have more than 1 primary key.
For example, there is an entity following under.
#Entity(primaryKeys = ["id"], tableName = "someEntity")
data class SomeEntity(
val id: Int = 0,
val someClass: SomeClass<*>? = null
)
#Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insert(obj: SomeClass): Completable
Parameter "obj" will have two column(fields).
When I have insert logic like that,
do I have to care about id (with autoGenerate annotation) column?
When I insert SomeEntity with dao,
I can only get SomeClass<*>? type, without id: Int.
Does #AutoGenerate annotation on column id can solve my problem?
Room understands val id: Int = 0 if it is marked with #PrimaryKey as a value to be ignored when inserting.
#Entity
data class SomeEntity(
#PrimaryKey(autoGenerate = true)
val id: Int = 0,
val someClass: SomeClass<*>? = null
)
and when creating a new instance SomeEntity(someClassInstance) is completely fine.
Note: if SomeClass isn't a basic class that SQL is able to save, you need to have some way to serialize it.
I am working on a small project using Room. Basically a Recipe can have many Ingredients and an Ingredient can belong to many Recipes.
These are my Entities as the Documentation in the many-to-many relationship part suggested:
#Entity
data class Recipe(
var name: String
) {
#PrimaryKey(autoGenerate = true)
var recipeUID: Int = 0
}
#Entity
data class Ingredient(
var name: String
) {
#PrimaryKey(autoGenerate = true)
var ingredientUID: Int = 0
}
#Entity(primaryKeys = ["recipeUID", "ingredientUID"])
data class RecipeIngredientCrossRef(
val recipeUID: Int,
val ingredientUID: Int
)
//Docs suggested #Junction but that causes error for using tag inside #Relation...
data class RecipeWithIngredients(
#Embedded
val recipe: Recipe,
#Relation(
parentColumn = "recipeUID",
entity = Ingredient::class,
entityColumn = "ingredientUID",
associateBy = Junction(
value = RecipeIngredientCrossRef::class,
parentColumn = "recipeUID",
entityColumn = "ingredientUID"
)
)
var ingredients: List<Ingredient>
)
Then I tried to make the Dao functions following the same documentation but it only describes how to retrieve, which I applied here:
#Transaction
#Query("SELECT * FROM recipe")
suspend fun getAllRecipes(): List<RecipeWithIngredients>
Then I found this Medium article explaining what I wanted to achieve, which lead me to write this for the Insert function:
#Insert(onConflict = REPLACE)
suspend fun insert(join: RecipeIngredientCrossRef): Long
Everything works up to the insertion, I have verified that the info is being inserted, however I am doing something wrong in retrieving since it returns nothing. I can see that the reason it returns nothing is because there was nothing inserted into that table (as my insert method places it into the RecipeIngredientCrossRef table and not the recipe table), which leads me to believe the part that is wrong is in fact my insertion method.
So basically my question is: How can I retrieve a list of RecipeWithIngredients object that contains a recipe object with a list of ingredient objects as a member variable?
The documentation on Room CRUD functionality does not mention anything about this type of relationships.
I have been looking for hours on Youtube, here, the documentation and Medium but so far nothing has worked. Can someone please shed some light or point me in the right direction.
Haven't you forgot to add insert-methods for your Recipe and Ingredient tables?
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipe(recipe: Recipe): Long
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertIngredient(ingredient: Ingredient): Long
So to get unempty RecipeWithIngredients result there should be three tables filled with data with your inserts from dao - Recipe, Ingredient, RecipeIngredientCrossRef. And of course they should contain id-s, that match to each other.
I have simple data object that I want to insert into room database but as I am using auto increment on my primary key I am doing it as below
#Dao
interface T1Dao {
#Query("INSERT INTO tbl_t1(data1, data2) VALUES ( :T1.data1, :T1.data2) ")
fun insert(note: T1): Long
}
I have many properties in T1 so I don't want pass them separately if possible.
In above example I am just showing two properties.
But you can just use #Insert and not to set your primary key field, can't you?
#Insert
fun insert(note: T1): Long
Let's say you have T1 class:
#Entity
data class T1(
#PrimaryKey(autoGenerate = true)
val id: Int = 0, // This lets you not to set id before inserting
val data1: String,
val data2: String
)
Then you can insert:
dao.insert(T1(data1 = "data1", data2 = "data2")) // just don't set id
The problem faced is to get calendar field from table.
My actual Data class is:
#TypeConverters(CalendarConverters::class)
#Entity(tableName = MY_TABLE)
data class MyEpg(
val updatedAt: Calendar,
val epgName: String,
val epgStartTime: Calendar,
val epgEndTime: Calendar,
#ColumnInfo(name = "calendar")
val calendar: Calendar,
val chName: String
) {
#ColumnInfo(name = "id")
#PrimaryKey(autoGenerate = true)
var id: Long = 0
}
I've used typeconverter as:
object CalendarConverters {
#TypeConverter
#JvmStatic
fun fromTimestamp(value: Long?): Calendar? = value?.let { value ->
GregorianCalendar().also { calendar ->
calendar.timeInMillis = value
}
}
#TypeConverter
#JvmStatic
fun toTimestamp(timestamp: Calendar?): Long? = timestamp?.timeInMillis
}
The query I used is:
#Query("select distinct ${MY_TABLE}.calendar from $MY_TABLE order by ${MY_TABLE}.calendar asc")
suspend fun getCalendars(): List<Calendar>
Error Shown in build view is
The query returns some columns [calendar] which are not used by
java.lang.Object. You can use #ColumnInfo annotation on the fields to
specify the mapping. You can suppress this warning by annotating the
method with #SuppressWarnings(RoomWarnings.CURSOR_MISMATCH). Columns
returned by the query: calendar. Fields in java.lang.Object: .
public abstract java.lang.Object getCalendars(#org.jetbrains.annotations.NotNull()
error: Not sure how to convert a Cursor to this method's return type
(java.lang.Object).
public abstract java.lang.Object getCalendars(#org.jetbrains.annotations.NotNull()
Note:
I've noticed that type converter to convert to long is being used while converting from long to date is not being used.
There is no any problem on getting any single fields or the complete object except for the single calendar item.
I reproduced your code but I'm not getting that error. However, I have some observations for you:
I noticed that as table name you use MY_TABLE but in the query you use My_TABLE with "lowercase y". Is this a typo?
You can avoid the use of #JvmStatic by replacing your object CalendarConverters to simple class. Object works with singleton pattern.
I'm supposing that the cause could be that you have invalid data inserted in the table MyEpg table, so then, when you do a request using your query, the column calendar contains a null value. That's could be the reason why you receive the next:
error: Not sure how to convert a Cursor to this method's return type (java.lang.Object). public abstract java.lang.Object getCalendars(#org.jetbrains.annotations.NotNull()
You could solve the problem by adding a nullable indicator to the return type:
#Query("select distinct ${MY_TABLE}.calendar from $MY_TABLE order by ${MY_TABLE}.calendar asc")
suspend fun getCalendars(): List<Calendar?>
Maybe you've forgotten to apply your TypeConverter (or have chosen wrong place to put the annotation)? There are two ways to do that in your case:
Apply it on Database-level:
#Database(entities = arrayOf(YourEntity::class), version = 1)
#TypeConverters(CalendarConverters::class)
abstract class AppDatabase : RoomDatabase() {
...................
Apply it on Entity level:
#Entity(tableName = "your_table")
#TypeConverters(CalendarConverters::class)
data class YourEntity (
and Dao level:
#TypeConverters(CalendarConverters::class)
suspend fun getCalendars(): List<Calendar>