Retrieve/insert many-to-many relationship object from Room (Kotlin) - android

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.

Related

What do #Relation classes mean when creating a one-to-many relationship in Room?

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

Room Dao Returns Insert IDs but Data is Missing from Database

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.

How to store a list of integers in Room database so that it can be easily queried later?

I'm storing podcast data in a Room database, where each podcast has a List<Int> called genreIds. I'd like to be able to store this in such a way that I can easily query it later by doing something like SELECT * FROM podcasts WHERE genreIds CONTAINS :genre, or whatever the command would be.
So what is the best way to store that list of Ints so that it can be easily queried later, and how would I do that using Room? I've used TypeConverters before, but that converts it to a string, which is difficult to query, so I'd like to be able to link it to another table or something that can be easily queried, I'm just not sure how to do that.
Thanks in advance.
The data stored on a the db with Room, depends on the data class you use. If you specify a data class with an Int member, that will be an Int on the db.
Example:
data class TrackPlayedDB (
#PrimaryKey(autoGenerate = true)
val _id: Int = 0,
val timesPlayed: Int,
val artist: String,
val trackTitle: String,
val playDateTime: LocalDateTime
)
here timesPlayed will be an Int on the DB (as _id). You'll specify your data classes like the following, this will build the corresponding tables.
#Database(entities = [TrackPlayedDB::class], version = 1, exportSchema = false)
#TypeConverters(Converters::class)
abstract class MyRoomDatabase : BaseRoomDatabase() {
Edit: Following author's comment, I stand corrected i didn't get the question right.
Author actually asks how to store a List<Int> as field on a table. There are 2 solutions to do that: one, as Author suggests, is to store the List as String and use Like keyword to write queries with a clause like the following:
SELECT * FROM mytable
WHERE column1 LIKE '%word1%'
OR column1 LIKE '%word2%'
OR column1 LIKE '%word3%'
as a simple search on SO would have shown: SQL SELECT WHERE field contains words
The Author says he used TypeConverters so i'll skip how to convert a List<Int> into a string
The other solution to this problem is to realise that nothing was understood about the theory of Transactional Databases. In fact, when you have a many-to-many relationship, as in the case of podcast and genre, theory dictates that you build a table that links the ids of podcasts and the ids of genres, as it is explained here: https://dzone.com/articles/how-to-handle-a-many-to-many-relationship-in-datab
and other countless books, videos and blogs.
This benefits the db with added clarity, performance and scalability.
Bottom line, Author's db design is wrong.
I found [this article on Medium][1] that I found very helpful. What I'm trying to do is a many to many relationship, which in this case would be done something like the following:
Podcast class:
#Entity(tableName = "podcasts")
data class Podcast(
#PrimaryKey
#ColumnInfo(name = "podcast_id")
val id: String,
// other fields
}
Genre class:
#Entity(tableName = "genres")
data class Genre (
#PrimaryKey
#ColumnInfo(name = "genre_id")
val id: Int,
val name: String,
val parent_id: Int
)
PodcastDetails class:
data class PodcastDetails (
#Embedded
val podcast: Podcast,
#Relation(
parentColumn = "podcast_id",
entityColumn = "genre_id",
associateBy = Junction(PodcastGenreCrossRef::class)
)
val genres: List<Genre>
)
PodcastGenreCrossRef:
#Entity(primaryKeys = ["podcast_id", "genre_id"])
data class PodcastGenreCrossRef (
val podcast_id: Int,
val genre_id: Int
)
And access it in the DAO like this:
#Transaction
#Query(SELECT * FROM podcasts)
fun getPodcastsWithGenre() : List<PodcastDetails>
[1]: https://medium.com/androiddevelopers/database-relations-with-room-544ab95e4542

Room Livedata returns incorrect values

I have an audio recorder app, where I enable the user to mark certain points in his recordings with predefined markers. For that purpose I have a MarkerEntity, which is the type of Marker, and a MarkTimestamp, the point at which the user marks a given recording. These entities are connected via a Relation, called MarkAndTimestamp.
#Entity(tableName = "markerTable")
data class MarkerEntity(
#PrimaryKey(autoGenerate = true) val uid: Int,
#ColumnInfo(name = "markerName") val markerName: String
)
#Entity(tableName = "markerTimeTable")
data class MarkTimestamp(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "mid") val mid: Int,
#ColumnInfo(name = "recordingId") val recordingId: Int,
#ColumnInfo(name = "markerId") val markerId: Int,
#ColumnInfo(name = "markTime") val markTime: String
)
data class MarkAndTimestamp(
#Embedded val marker: MarkerEntity,
#Relation(
parentColumn = "uid",
entityColumn = "markerId"
)
val markTimestamp: MarkTimestamp
)
The insertion of this data works flawlessly, I checked this via DB Browser for SQLite and Android Debug Database. The problem arises, when I want to display all marks for a recording. I fetch the entries with the following SQL statement.
#Transaction
#Query("SELECT * FROM markerTimeTable INNER JOIN markerTable ON markerTimeTable.markerId=markerTable.uid WHERE markerTimeTable.recordingId = :key")
fun getMarksById(key: Int): LiveData<List<MarkAndTimestamp>>
What ends up happening is, that if the user uses a Marker more than once, all marks created with that Marker have the same MarkerTimestamp row attached to them, specificially, the last row to be inserted with that Marker. The weird thing is, this only happens in the app using Livedata. Using the same query in DB Browser for SQLite returns the correct and desired data.
This is the stored data (correct)
MarkTimestamps
MarkerEntities
And this is the Livedata returned at this point (incorrect)
[
MarkAndTimestamp(marker=MarkerEntity(uid=1, markerName=Mark), markTimestamp=MarkTimestamp(mid=6, recordingId=2, markerId=1, markTime=00:05)),
MarkAndTimestamp(marker=MarkerEntity(uid=2, markerName=zwei), markTimestamp=MarkTimestamp(mid=5, recordingId=2, markerId=2, markTime=00:03)),
MarkAndTimestamp(marker=MarkerEntity(uid=1, markerName=Mark), markTimestamp=MarkTimestamp(mid=6, recordingId=2, markerId=1, markTime=00:05))
]
I also get the following build warning
warning: The query returns some columns [mid, recordingId, markerId, markTime] which are not used by de.ur.mi.audidroid.models.MarkAndTimestamp. 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: mid, recordingId, markerId, markTime, uid, markerName. Fields in de.ur.mi.audidroid.models.MarkAndTimestamp: uid, markerName. - getMarksById(int) in de.ur.mi.audidroid.models.MarkerDao
Why does Room return the wrong data and how do I fix this?
So, I still don't know what caused the behaviour described in my original post. My guess is, that the realtion data class and the SQL query interfered in some way, producing the cinfusing and incorrect outcome.
I solved my problem nonetheless. I needed to change
data class MarkAndTimestamp(
#Embedded val marker: MarkerEntity,
#Relation(
parentColumn = "uid",
entityColumn = "markerId"
)
val markTimestamp: MarkTimestamp
)
to
data class MarkAndTimestamp(
#Embedded val marker: MarkerEntity,
#Embedded val markTimestamp: MarkTimestamp
)
This makes sure, that all fields returned by the query are included in the data class.

Android Room [SQLITE_ERROR] SQL error or missing database when using Relation and Junction

this is my first question here.
I'm building an app using Room database and I tried following this tutorial because I need to implement a many-to-many relation.
However I keep getting the following error as soon as I try to build the app:
error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such table: OwnerDogCrossRef)
private final java.util.List dogs= null;
My entities, with the cross reference data class:
#Entity(tableName = "owner_table")
data class Owner(
#ColumnInfo(name = "owner_id")
val id: String,
#PrimaryKey
#ColumnInfo(name = "owner_name", index = true)
val name: String,
// some other columns
#Entity(tableName = "dog_table")
data class Dog(
#PrimaryKey
#ColumnInfo(name = "dog_name")
val name: String
// some other columns
#Entity(primaryKeys = ["owner_name", "dog_name"])//, "move_learned_by"])
data class OwnerDogCrossRef(
val owner_name: String,
#ColumnInfo(index = true)
val dog_name: String
// some other columns
My junction data class:
data class OwnerWithDogs(
#Embedded val owner: Owner,
#Relation(
parentColumn = "owner_name",
entityColumn = "dog_name",
associateBy = Junction(OwnerDogCrossRef::class)
)
val dogs: List<Dog>
)
My DAO:
#Dao
inteface OwnerDao {
#Transaction
#Query("SELECT * FROM owner_table WHERE owner_name = :name")
fun getOwnerWithDogs(name: String): LiveData<List<OwnerWithDogs>>
}
I also added the OwnerDogCrossRef to my database as below:
#Database(
entities = [Owner::class, Dog::class, OwnerDogCrossRef::class],
version = 2,
exportSchema = false
)
#TypeConverters(Converters::class)
abstract class MainDatabase : RoomDatabase() {
//some logic
}
Thank you for your help
Go to your Database.kt file and make sure the cross-reference table has been included in the list of entities there. Yours may differ to the example below, but I hope you can see what you may have missed.
#Database(entities = [Owner::class, Dog::class, OwnerDogCrossRef::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
...
}
This is because the Owner, Dog, and OwnerDogCrossRef are all tables that the database needs to know about, whereas OwnerWithDogs is merely going to join the relevant tables in a transaction, as multiple queries will be run, as per documentation. See also here for the database documentation showing how to include the entities for a particular database.
data class OwnerWithDogs(
#Embedded val owner: Owner,
#Relation(
parentColumn = "owner_name",
entity = Dog.class,
entityColumn = "dog_name",
associateBy = #Junction(
value=OwnerDogCrossRef.class,
parentColumn = "owner_name",//variable name in your OwnerDogCrossRef
entityColumn = "dog_name")
)
val dogs: List<Dog>
)
Try this code.
For further ideas ,try this link
https://developer.android.com/reference/androidx/room/Junction

Categories

Resources