I have my entity object this way:
#Entity(tableName = "woks", foreignKeys = arrayOf(ForeignKey(
entity = Order::class,
parentColumns = arrayOf("entryid"),
childColumns = arrayOf("order_id"),
onDelete = ForeignKey.CASCADE
)))
data class Wok(
val order_id: String
) {
#ColumnInfo(name = "id")
#PrimaryKey(autoGenerate = true)
var id: Long = 0
}
and the insertion in my DAO file defined this way:
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insertWok(wok: Wok): Long
As you can see the conflict is ignored so when ever I tried to insert the same object again it will override that object and keep increment the id.
So what I want to achieve is inserting the same object so many times
To insert your object many times, you need to change the primary key.
Room is based on it to know if it's a new object or not.
I have tried many combination which are possible with your given information, but i didn't came with anything as you are saying.
Can u please share more information like your contructor, how you are creating work object,
actually what happen with output (before-after)
This may be happen,
if your work object id is 0 while passing to your insertWork method
if u put 2 identical object with id 0 of both, because of autogenrate=true room will autogenrate id for them, and as room will create unique id, these object treat as different object.
Related
On the bottom I added a picture of my current app structure and the current code for included data classes / entities.
At the moment in my app the user inserts the url and the code in the Login Fragment, clicking on save button the request to get the token starts. When successful the token is passed to the other requests to fetch the categories data. The different categories I get from the response are then showed in a recyclerview. By clicking on a category the user comes to the movies / seriers by genre Fragment, there I have another recyclerview with the list of movies or series.
When the token-request is successful the url & code are send also to a data class (entity) called AccountData, additional there is a unique String, put together of the url and the code, which works as Primary Key.
The AccountData is shown in a recyclerview on the Account Managment Fragment, which is the start screen of the app.
Now I want to give the user the option to select for each account, the categories he wants to have shown. Having the possibility to modify he's preferences everytime he wants.
For example:
AccountA has 10 movie categories, the user wants have shown only 5 of them.
AccountB has 15 movie categories, the user wants have shown only 6 of them.
My idea is to create a new Fragment, MovieCategorySelectFragment or so, where the user can click the categories he wants, passing the selected categories to the Movies Categories Fragment, like a Favorite-List. For implementing this I think about Room.
So I made the MovieCategory data class an Entity, using "Id" as Primary Key, and then, considering that it is a one to many relationship (I hope I am right with this), I added the Primary Key from the AccountData Entity to the MovieCategory Entity.
I made the String nullable -> val accountData: String?, that I don't get the NullpointerException error.
But now I'm stuck, would it be better to create a new data class / entity, calling it f.e. SelectedMovieCategory, and passing to it the selected item/category (from the MovieCategorySelectFragment, which is not part of a database) to it and using the room database then do display the selectet categories in the Movies Categories Fragment. Or should I make the request for the categories and save them immediately in the room-database and handling then the process of selecting?
And finally, on both methods, how can I pass the primary key from AccountData to MovieCategory? Otherwise there is no relationship between them? Do I have to create a function in the Dao to handle this?
At the end in the Account Management Fragment the user should be able to click on the account he wants to load, having loaded for each account only the categories he selected before. With the ability to change his preferences going into the MovieCategorySelectFragment and add or remove some categories from his "favorite-list".
Hopefully someone can help me to find the best and easiest way to handle this.
These are the data classes:
data class MovieCategoryResponse(
val js: List<MovieCategory>
)
#Entity
#Parcelize
data class MovieCategory(
#PrimaryKey(autoGenerate = false)
val id: String,
val number: Int,
val title: String,
val accountData: String?
) : Parcelable
#Entity
data class AccountData(
val url: String,
val code: String,
#PrimaryKey(autoGenerate = false)
val totalAccountData: String
)
It appears that you want accounts, moviecategories, seriescategories and a means of associating/relating accounts with a number of moviecategories and perhaps seriescategories and to be able to restrict the number of listed moviecategories according to an accounts preference which can change.
The solution would be a many-many relationship between account and moviecategories (and perhaps seriescategories).
However, before moving on you say:-
So I made the MovieCategory data class an Entity, using "Id" as Primary Key, and then, considering that it is a one to many relationship (I hope I am right with this), I added the Primary Key from the AccountData Entity to the MovieCategory Entity.
Regarding your hope. I believe that you are wrong. There are 3 types of relationships:-
1-1 where each row in a table would have a means of uniquely identifying the row, typically via a single column, in other table (if a separate table is apt (a single table may well suffice)). 1-1 relationships are not typically catered for by using tables.
1-many where a row in the parent table (account) could have many children (moviecategory) BUT the moviecategory could only have the 1 parent. In such a case a column in the moviecategory would contain a value that uniquely identifies the parent.
many-many an expanded 1-M that allows each side to relate to any number of the other side. So an account can relate to many moviecategories to which other accounts can relate to. The typical solution is to have an intermediate table that has 2 core columns. One that stores the value that uniquely identifies one side of the relationship and the other that stores the value that uniquely identifies the other side. Typically the 2 columns would for the primary key.
such an intermediate table has numerous terms to describe such a table, such as associative table, mapping table, reference table ....
Note how id has been highlighted. Simply creating a column called id in a table (Entity) does not make a relationship, it only supports the potential of a relationship being made.
You problem would appear to tick the boxes for a many-many relationship and thus the extra table (2 if account-secriescategories).
This table would have a column for the the value that uniquely identifies the accountData row (totalAccountData).
as the totalAccountData is the primary key (i.e. it is annotated with #PrimaryKey) and that a PrimaryKey is implicitly unique
The table would have a second column for the movieCategory's id column.
So you could start with
#Entity
data class AccountMovieMap(
val accountDataMap: String,
val movieCategoryMap: String
)
However, there is no PrimaryKey which room requires BUT the #PrimaryKey annotation only applies to a single column. If either were used then due to the implicit uniqueness the relationship would be restricted to 1-many. A composite (multiple columns/values) Primary Key is required that makes the uniqueness according to the combined values. To specify a composite PrimaryKey in Room the primaryKeys parameter of the #Entity annotation is used.
So AccountMovieMap becomes :-
#Entity(
primaryKeys = ["accountDataMap","movieCategoryMap"]
)
data class AccountMovieMap(
val accountDataMap: String,
val movieCategoryMap: String
)
As it stands there is a potential issue with the above as it is possible to insert data into either or both columns that is not a value in the respective table. That is the integrity of the relationship, in such a situation, does not exist.
SQLite and therefore Room (as with many relational database) caters for enforcing Referential Integrity. SQLite does this via ForeignKey clauses. Room uses the foreignKeys parameter of the #Entity annotation to provide a list of ForeignKeys.
In addition to enforcing referential integrity SQlite has 2 clauses ON DELETE and ON UPDATE that help to maintain referential integrity (depending upon the specified action, the most useful being CASCADE which allows changes that would break referential integrity by applying changes to the parent to the children).
Room will also warn if an index does not exist where it believe one should e.g. warning: movieCategoryMap column references a foreign key but it is not part of an index. This may trigger full table scans whenever parent table is modified so you are highly advised to create an index that covers this column. As such, the #ColumnInfo annotation can be used to add an index on the movieCategoryMap column.
So AccountMovieMap could be the fuller:-
#Entity(
primaryKeys = ["accountDataMap","movieCategoryMap"]
, foreignKeys = [
ForeignKey(
entity = AccountData::class,
parentColumns = ["totalAccountData"],
childColumns = ["accountDataMap"],
/* Optional but helps to maintain Referential Integrity */
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = MovieCategory::class,
parentColumns = ["id"],
childColumns = ["movieCategoryMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class AccountMovieMap(
val accountDataMap: String,
#ColumnInfo(index = true)
val movieCategoryMap: String
)
To add (insert) rows you could then have/use (in an #Dao annotated class):-
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(accountMovieMap: AccountMovieMap)
noting that to avoid referential integrity conflicts that the accountData referenced/mapped and MovieCategory referenced/mapped need to exist.
As you would want to extract an AccountData's MovieCategories you need a POJO that has the AccountData with a List of MovieCategory.
This could be:-
data class AccountWithMovieCategoryList(
#Embedded
val accountData: AccountData,
#Relation(
entity = MovieCategory::class,
parentColumn = "totalAccountData", /* The column referenced in the #Embedded */
entityColumn = "id", /* The column referenced in the #Relation (MovieCategory) */
/* The mapping table */
associateBy = (
Junction(
value = AccountMovieMap::class, /* The #Entity annotated class for the mapping table */
parentColumn = "accountDataMap", /* the column in the mapping table that references the #Embedded */
entityColumn = "movieCategoryMap" /* the column in the mapping table that references the #Relation */
)
)
)
val movieCategoryList: List<MovieCategory>
)
The following could be the function in an #Dao annotated interface that retrieves an AccountWithMovieCategoryList for a given account:-
#Transaction
#Query("SELECT * FROM accountData WHERE totalAccountData=:totalAccountData")
fun getAnAccountWithMovieCategoryList(totalAccountData: String): List<AccountWithMovieCategoryList>
However Room will retrieve ALL the MovieCategories but you want to be able to specify a LIMITed number of MovieCategories for an Account, so a means is required to override Room's methodology of getting ALL mapped/associate objects.
To facilitate this then a function with a body can be used to a) get the respective AccountData and b) to then get the MovieCategory list according to the account, via the mapping table with a LIMIT specified. Thus 2 #Query functions to doe the 2 gets invoked by the overarching function.
So to get the AccountData:-
#Query("SELECT * FROM accountData WHERE totalAccountData=:totalAccountData")
fun getSingleAccount(totalAccountData: String): AccountData
And then to get the limited MovieCategories for an AccountData via (JOIN) the mapping table :-
#Query("SELECT movieCategory.* FROM accountMovieMap JOIN movieCategory ON accountMovieMap.MovieCategoryMap = movieCategory.id WHERE accountMovieMap.accountDataMap=:totalAccountData LIMIT :limit")
fun getLimitedMovieCategoriesForAnAccount(totalAccountData: String,limit: Int): List<MovieCategory>
And to put it all together aka the overarching function:-
#Transaction
#Query("")
fun getAccountWithLimitedMovieCategoryList(totalAccountData: String,categoryLimit: Int): AccountWithMovieCategoryList {
return AccountWithMovieCategoryList(
getSingleAccount(totalAccountData),
getLimitedMovieCategoriesForAnAccount(totalAccountData,categoryLimit)
)
}
Note the above code has only been compiled (so Room processing sees no issues), as such it is in-principle code
You say Best, this is opiniated and is not the best as a better way would be to utilise SQLite's more efficient handling of INTEGER primary keys.
categoryLimit i.e. an Int passed could be dynamically via the UI selected or stored and thus preserved. It could be stored in the AccountData by adding a suitable column or it could be stored elsewhere. AccountData would seem the simplest and most apt unless the expectation is that many such account based preferences should exist.
If, for example, an extra column was added to AccountData e.g. :-
#Entity
data class AccountData(
val url: String,
val code: String,
#PrimaryKey(autoGenerate = false)
val totalAccountData: String,
val movieCategoryLimit: Int /*<<<<< to store LIMIT preference */
)
A means of changing the limit would very likely be required such as the following in a/the #Dao annotated interface :-
#Query("UPDATE accountData SET movieCategoryLimit=:newLimit WHERE totalAccountData=:totalAccountData")
fun changeMovieCategoryLimit(totalAccountData: String, newLimit: Int)
A subtle change to the getAccountWithLimitedMovieCategoryList function and the LIMIT is as per the preference:-
#Transaction
#Query("")
fun getAccountWithLimitedMovieCategoryList(totalAccountData: String,categoryLimit: Int): AccountWithMovieCategoryList {
val accountData = getSingleAccount(totalAccountData)
return AccountWithMovieCategoryList(
accountData,
getLimitedMovieCategoriesForAnAccount(totalAccountData,accountData.movieCategoryLimit)
)
}
i.e. rather than using the retrieved AccountData directly, it is retrieved into a val, the val is then used to provide the AccountData and then again to provide the value for the LIMIT.
Additional
As per the comment
.... Isn't it then a one-to-many relation? ....
Then for a 1-M, as previously explained MovieCategory should have a column for storing a unique column. Which I guess is what you index the val accountData: String? to be for.
The ? should never be null (an orphan that is basically useless). Ideally, as the column use would imply selection via this column, the column should be indexed. As the intended use is as a foreign key, then although not required defining it as a foreign and enforcing referential integrity makes sense (and also goes towards describing/comment the column as a foreign key). Then the MovieCategory class could be
:-
/* MovieCategory modified for 1-m (1 account many categories)*/
#Entity(
foreignKeys = [
ForeignKey(
entity = AccountData::class,
parentColumns = ["totalAccountData"],
childColumns = ["accountData"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
)
]
)
#Parcelize
data class MovieCategory(
#PrimaryKey(autoGenerate = false)
val id: String,
val number: Int,
val title: String,
#ColumnInfo(index = true) /* added as likely to used for selecting rows */
val accountData: String /* should NEVER by null */
) : Parcelable
Assuming that AccountData includes the movieCategoryLimit (as explained above) then a POJO for retrieving the Account and the LIMITED list of MovieCategories would be required (a little different to the m-m equivalent) which could be:-
data class AccountDataWithMovieCategories(
#Embedded
val accountData: AccountData,
#Relation(
entity = MovieCategory::class,
parentColumn = "totalAccountData",
entityColumn = "accountData"
)
val movieCategories: List<MovieCategory>
)
i.e. no association table involved
The same issue exists that Room will retrieve ALL MovieCategories. So the following is not what you want:-
/* Note will get ALL MovieCategories (even with join and LIMIT) */
#Transaction
#Query("SELECT * FROM accountData WHERE totalAccountData=:totalAccountData")
fun getAccountWithMovieCategories(totalAccountData: String): List<AccountWithMovieCategoryList>
Rather you want to have:-
#Query("SELECT * FROM movieCategory WHERE accountData=:totalAccountData LIMIT :limit")
fun getLimitedMoviesCategoriesPerAccount(totalAccountData: String, limit: Int): List<MovieCategory>
i.e. directly accessing the MovieCategory rather than via the JOIN on the association table in the m-m version
An then finally the overarching function:-
#Transaction
#Query("")
fun getAccountWithLimitedMovieCategories(totalAccountData: String, limit: Int): AccountDataWithMovieCategories {
val accountData = getSingleAccount(totalAccountData)
return AccountDataWithMovieCategories(
accountData,
getLimitedMovieCategoriesForAnAccount(
accountData.totalAccountData,
accountData.movieCategoryLimit /* the limit as stored in the account */
)
)
}
#MikeT
Sorry for the late response...(I am using an answer because It's to long for a comment and editing my question would be messy)
I was nearly able to get everything I wanted, using this way (example series categories):
I saved the fetched categories (using retrofit) in a new POJO, named SeriesCategory:
#Entity(
foreignKeys = [
ForeignKey(
entity = AccountData::class,
parentColumns = ["totalAccountData"],
childColumns = ["accountData"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
)
]
)
#Parcelize
data class SeriesCategory(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
#ColumnInfo(index = true)
var accountData: String,
var favorite: Boolean,
val idByAccountData: String
) : Parcelable
using...
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSeriesCategory(seriescategory: List<SeriesCategory>)
val mappedSeriesCategoryList = seriescatresponse.js.map { SeriesCategorybyAccount(it.id, it.title, totalAccountData, isFavorite, "${it.id}$totalAccountData") }
lifecycleScope.launch {
mappedSeriesCategoryList.forEach { dao.insertSeriesCategory(mappedSeriesCategoryList) }
}
In the Dao & SeriesCategoryFragment I use following code to get all categories from this specific account:
#Query("SELECT * FROM seriesCategory WHERE accountData=:totalAccountData")
fun getSeriesCategoriesPerAccount(totalAccountData: String): LiveData<List<SeriesCategory>>
viewModel.getSeriesCategoryByAccount(totalAccountData, this#SeriesCategoryFragment.requireActivity()).observe(viewLifecycleOwner) {
seriesCategoryAdapter.submitList(it)
}
As I am using a checkbox in the SeriesCategoryFragment-recyclerview I managed the checkbox in the adapter and used following code in the Dao & SelectedSeriesCategoryFragment:
#Query("SELECT * FROM seriesCategory WHERE accountData=:totalAccountData AND favorite = 1")
fun getSelectedSeriesCategoriesPerAccount(totalAccountData: String): LiveData<List<SeriesCategory>>
viewModel.getSelectedSeriesCategoryByAccount(totalAccountData, this#SeriesSelectedFragment.requireActivity()).observe(viewLifecycleOwner) {
selectedSeriesCategoryAdapter.submitList(it)
}
Thats working fine, when in the SeriesCategoryFragment a checkbox is checked(favorite = true), the category is "send" to the SelectedSeriesCategoryFragment, if it's unchecked (favorite = false) it's removed.
But at the moment I am not able to save this in my room database. So when I have for example following categories:
CategoryA, CategoryB, CategoryC, CategoryD
The user checks CategoryA and CategoryC using the checkbox, this two categories are then displayed in the SelectedSeriesCategoryFragment-recyclerview. When he unchecks the checkbox the category is removed. That's all ok.
In the same clicklistener I am using also #Update with (it = SeriesCategory):
viewModel.updateSeriesCategory(it, this#SeriesCategoryFragment.requireActivity())
Restarting the app the database shows me favorite = 1 (for true) on the categories that where checked before the restart. But the checkboxes are unchecked - an the #Query I use in the SelectedSeriesCategoryFragment (see above) doesn't seem to work anymore -> means, it is empty.
On a second restart the database shows all categories as favorite = 0 (for false) - probably because the checkbox was empty before the second restart. So as I understand, the idea I have should work - but only if the checked Checkboxes stay checked also after the restart. Can I handle this somehow with room?
Or is this a completely recyclerview-specific problem?
Eventually, my Viewholder in the Adapter looks like this:
inner class ViewHolder(val binding: RvItemSeriescategoryBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(category: SeriesCategory) {
binding.apply {
rvItemSeriescategory.text = category.title
checkboxSeriescategory.isChecked = category.favorite
checkboxSeriescategory.setOnClickListener {
if (checkboxSeriescategory.isChecked) {
category.favorite = true
onClickListener.onClick(category)
}
if (!checkboxSeriescategory.isChecked)
category.favorite = false
onClickListener.onClick(category)
}
if (category.favorite == true){
checkboxSeriescategory.isChecked = true
}
}
}
}
I'm making an Workout log app.
One Workout has multiple sets.
I want to store this in a one-to-many relationship in Room.
In conclusion, I succeeded in saving, but I'm not sure what one class does.
All of the other example sample code uses this class, so I made one myself, but it doesn't tell me what it means.
WorkoutWithSets
data class WorkoutWithSets(
#Embedded val workout: Workout,
#Relation (
parentColumn = "workoutId",
entityColumn = "parentWorkoutId"
)
val sets: List<WorkoutSetInfo>
)
The following two entity classes seem to be sufficient to express a one-to-many relationship. (Stored in Room)
Workout
#Entity
data class Workout(
#PrimaryKey(autoGenerate = true)
var workoutId: Long = 0,
var title: String = "",
var memo: String = "",
)
It seems that the following two entity classes are sufficient enough to store a one-to-many relationship.. (stored in Room)
WorkoutSetInfo
#Entity(
foreignKeys = [
ForeignKey(
entity = Workout::class,
parentColumns = arrayOf("workoutId"),
childColumns = arrayOf("parentWorkoutId"),
onDelete = ForeignKey.CASCADE
)
]
)
data class WorkoutSetInfo(
#PrimaryKey(autoGenerate = true)
val id: Long = 0,
val set: Int,
var weight: String = "",
var reps: String = "",
var unit: WorkoutUnit = WorkoutUnit.kg,
val parentWorkoutId: Long = 0
)
Even if the WorkoutWithSet class does not exist, the Workout and WorkoutSetInfo classes are stored in Room.
What does WorkoutWithSets class mean? (or where should I use it?)
What does WorkoutWithSets class mean?
It is a class that can be used to retrieve a Workout along with all the related WorkoutSetInfos via a simple #Query that just retrieves the parent Workouts.
What Room does is add an additional query that retrieves the children (WorkoutSetInfo's) for each Workout.
The result being a list of WorkOutWithSets each element (a Workout) containing/including a list of all the related WorkoutSetInfo's.
You would use this when you want to process a Workout (or many Workouts) along with the related WorkoutSetInfo's (aka the child WorkoutSetInfo's for the parent Workout).
What Room does is consider the type (objects) to be returned.
So if you had
#Query("SELECT * FROM workout")
fun getJustWorkouts(): List<Workout>
then the function would return just a list of Workout objects.
But if you had
#Query("SELECT * FROM workout")
fun getWorkoutsWithSets(): List<WorkoutWithSets>
then the function would return a list of WorkoutWithSets and thus the parent Workouts with the child WorkoutSetInfo's.
What Room does is build and execute an underlying query, for eack Workout extracted, along the lines of "SELECT * FROM workoutInfoSet WHERE workout.workoutId = parentWorkoutId" and hence why it suggests the use of the #Transaction annotation (the build will include a warning if #Transaction is not coded).
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.
When saving a list of objects in my room database using a Dao
#Insert()
fun saveCharmRankMaterialCosts(materialCosts: List<CharmRankCraftingCost>) : List<Long>
And this is used from my repository class to save results from an API call:
val charmRankCosts = CharmRankCraftingCost.fromJsonCraftingCost(
charmRankId.toInt(),
jsonCharmRank.crafting
)
// save crafting/upgrade costs for the rank
val results = charmDao.saveCharmRankMaterialCosts(charmRankCosts)
Log.d("CharmRepository", "Saved charm material costs: ${results.toString()}");
assert(!results.contains(-1))
When running this code, insert ID's are returned and the assertion is never triggered (i.e. no inserts fail).
But when I inspect the data base on the device, most of the supposedly inserted IDs are missing from the table. I'm very confused as to what is going on here. I've debugged this issue for many hours and have been unsuccessful in getting this to work. Is there something obvious I'm missing?
The issue seems to have been related to foreign key constraints. I had a CharmRank data class with multiple related data objects. See below:
/**
* Copyright Paul, 2020
* Part of the MHW Database project.
*
* Licensed under the MIT License
*/
#Entity(tableName = "charm_ranks")
data class CharmRank(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "charm_rank_id")
var id: Int = 0,
#ColumnInfo(name = "charm_id")
var charmId : Int,
#ColumnInfo(name = "charm_rank_level")
var level: Int = 0, // 3
#ColumnInfo(name = "charm_rank_rarity")
var rarity: Int = 0, // 6
#ColumnInfo(name = "charm_rank_name")
var name: String = "",
#ColumnInfo(name = "craftable")
var craftable: Boolean
)
Each charm rank has associated skills and items to craft said rank. These objects are simply relational objects in that they hold the ID of the CharmRank and a SkillRank in the case of the skills object, or the ID of the CharmRank and the ID of the Item object.
data class CharmRankSkill(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "charm_rank_skill_id")
var id: Int,
var charmRankId : Int,
var skillRankId: Int
)
data class CharmRankCraftingCost(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "charm_rank_crafting_cost_id")
var id: Int,
#ColumnInfo(name = "charm_rank_id")
var charmRankId: Int,
#ColumnInfo(name = "charm_rank_crafting_cost_item_quantity")
val quantity: Int,
val itemId: Int
)
Originally in CharmRankCraftingCost, I had a foreign key constraint on the Item object and the CharmRank object. Below is the foreign key constraint on the Item object:
ForeignKey(
entity = Item::class,
parentColumns = ["item_id"],
childColumns = ["itemId"],
onDelete = ForeignKey.CASCADE
)
The Item data object has IDs provided by the remote data source, so when I insert items into it's respective table, the conflict resolution is set to Replace. During the process of saving the relational items to the data base for the CharmRanks, I also have to save the Item objects prior to saving CharmRankCraftingCosts. It seems that what was happening is that when the Item objects are inserted, sometimes the items would get replaced, which would trigger the cascade action of the foreign key resulting in the CharmRankCraftingCosts items I just saved for the CharmRank to be deleted due to the cascading effect.
Removing the foreign key constraint on the Item table solved my issue.
As I understood from the comments, you make a delete before inserts. The problem is that it happens that the insert gets completed before the delete since you do them in separate threads. What you need is to do both in one transaction. Create a method in the the DAO class with #Transaction annotation (Make sure your dao is an abstract class so you can implement the body of this method):
#Dao
public abstract class YourDao{
#Insert(onConflict = OnConflictStrategy.IGNORE)
public abstract List<Long> insertData(List<Data> list);
#Query("DELETE FROM your_table")
public abstract void deleteData();
#Transaction
public void insertAndDeleteInTransaction(List<Data> list) {
// Anything inside this method runs in a single transaction.
deleteData();
insertData(list);
}
}
Read this for Kotlin Version of the code.
I can't understand difference between those annotations.
In my use case i want create one-to-many relation between tables.
And found two options: one with #ForeignKey and another with #Relation
Also i found that if i update the row (e.g. with OnCoflictStrategy.Replace) i will lost foreign key for this row is it true?
While both of these concepts are used to bring structure to your Room database, their use case differs in that:
#ForeignKey is used to enforce relational structure when INSERTING / MODYFING your data
#Relation is used to enforce relational structure when RETRIEVING / VIEWING your data.
To better understand the need for ForeignKeys consider the following example:
#Entity
data class Artist(
#PrimaryKey val artistId: Long,
val name: String
)
#Entity
data class Album(
#PrimaryKey val albumId: Long,
val title: String,
val artistId: Long
)
The applications using this database are entitled to assume that for each row in the Album table there exists a corresponding row in the Artist table. Unfortunately, if a user edits the database using an external tool or if there is a bug in an application, rows might be inserted into the Album table that do not correspond to any row in the Artist table. Or rows might be deleted from the Artist table, leaving orphaned rows in the Album table that do not correspond to any of the remaining rows in Artist. This might cause the application or applications to malfunction later on, or at least make coding the application more difficult.
One solution is to add an SQL foreign key constraint to the database schema to enforce the relationship between the Artist and Album table.
#Entity
data class Artist(
#PrimaryKey val id: Long,
val name: String
)
#Entity(
foreignKeys = [ForeignKey(
entity = Artist::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("artistId"),
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)]
)
data class Album(
#PrimaryKey val albumId: Long,
val title: String,
val artistId: Long
)
Now whenever you insert a new album, SQL checks if there exists a artist with that given ID and only then you can go ahead with the transaction. Also if you update an artist's information or remove it from the Artist table, SQL checks for any albums of that artist and updates / deletes them. That's the magic of ForeignKey.CASCADE!
But this doesn't automatically make them return together during a Query, so enter #Relation:
// Our data classes from before
#Entity
data class Artist(
#PrimaryKey val id: Long,
val name: String
)
#Entity(
foreignKeys = [ForeignKey(
entity = Artist::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("artistId"),
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)]
)
data class Album(
#PrimaryKey val albumId: Long,
val title: String,
val artistId: Long
)
// Now embedded for efficient querying
data class ArtistAndAlbums(
#Embedded val artist: Artist,
#Relation(
parentColumn = "id",
entityColumn = "artistId"
)
val album: List<Album> // <-- This is a one-to-many relationship, since each artist has many albums, hence returning a List here
)
Now you can easily fetch list of artists and their albums with the following:
#Transaction
#Query("SELECT * FROM Artist")
fun getArtistsAndAlbums(): List<ArtistAndAlbums>
While previously you had to write long boilerplate SQL queries to join and return them.
Note: The #Transaction annotation is required to make SQLite execute the two search queries (one lookup in the Artist table and one lookup in the Album table) in one go and not separately.
Sources:
Excerpts from Android Developers Documentations:
Sometimes, you'd like to express an entity or data object as a cohesive whole in your database logic, even if the object contains several fields. In these situations, you can use the #Embedded annotation to represent an object that you'd like to decompose into its subfields within a table. You can then QUERY the embedded fields just as you would for other individual columns.
Foreign keys allows you to specify constraints across Entities such that SQLite will ensure that the relationship is valid when you MODIFY the database.
SQLite's ForeignKey documentation.
A #ForeignKey defines a constraint (aka rule) that requires that the child column(s) exist in the parent column(s). If an attempt is made to break that rule then a conflict occurs (which may be handled various ways by the onDelete/onUpdate definition).
An #Relationship is used to define a relationship where a number of child (perhaps Foreign Key children) objects are returned in the parent object.
Underneath it all #Relation automatically (effectively) joins the tables and generates the number of child objects. Whilst a #ForeignKey just affects the schema (with the exception of onDelete/onUpdate handling), it does not result in the respective tables being joined.
Perhaps Consider the following :-
Service Entity
#Entity(
tableName = "services"
)
class Services {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "services_id")
var id: Long = 0
var service_date: String = ""
var user_mobile_no: String = ""
}
and
ServiceDetail Entity :-
#Entity(
tableName = "service_detail",
foreignKeys = [
ForeignKey(
entity = Services::class,
parentColumns = ["services_id"],
childColumns = ["services_id"],onDelete = ForeignKey.SET_DEFAULT
)
]
)
class ServiceDetail {
#PrimaryKey
var id: Long? = null;
var services_id: Long = 0;
#ColumnInfo(defaultValue = "1")
var service_type_id: Long = 0;
constructor()
#Ignore
constructor(services_id: Long, service_type_id: Long) {
this.services_id = services_id
this.service_type_id = service_type_id
}
}
This is saying that in order to add a ServiceDetail then the value of the services_id column MUST be a value that exists in the services_id column of the services Table, otherwise a conflict will happen. And additionally if a row is deleted from the services table then any rows in the service_detail table that reference that row will also be deleted (otherwise the row couldn;t be deleted from the services table).
Now consider this normal class (POJO), which is NOT an entity (aka table) :-
class ServiceWithDetail {
#Embedded
var services: Services? = null
#Relation(entity = ServiceDetail::class,parentColumn = "services_id",entityColumn = "services_id")
var serviceDetail: List<ServiceDetail>? = null
}
This is roughly saying when you ask for a ServiceWithDetail object then get a services object along with a list of the related service_detail objects
You would have a Dao such as :-
#Query("SELECT * FROM services")
fun getAllServices() :List<ServiceWithDetail>
So it will get all the services from the services table along with the related (i.e. where the services_id in the services_detail is the same as the services_id of the current services row being processed).
onConflictStrategy
REPLACE does the following :-
When a UNIQUE or PRIMARY KEY constraint violation occurs, the REPLACE
algorithm deletes pre-existing rows that are causing the constraint
violation prior to inserting or updating the current row and the
command continues executing normally.
If a NOT NULL constraint
violation occurs, the REPLACE conflict resolution replaces the NULL
value with the default value for that column, or if the column has no
default value, then the ABORT algorithm is used. If a CHECK constraint
or foreign key constraint violation occurs, the REPLACE conflict
resolution algorithm works like ABORT.
When the REPLACE conflict resolution strategy deletes rows in order to
satisfy a constraint, delete triggers fire if and only if recursive
triggers are enabled.
The update hook is not invoked for rows that are deleted by the
REPLACE conflict resolution strategy. Nor does REPLACE increment the
change counter. The exceptional behaviors defined in this paragraph
might change in a future release.REPLACE
Hence, the potential for the behaviour that you have experienced. However, it depends upon what the update is doing. If the value for the ForeignKey(s) differ then they should, assuming there is no Foreign Key conflict replace the foreign key value with the new valid value. If the Foreign Key value(s) is(are) unchanged then replacement row will have the same Foreign Keys.