I have a problem with my Room database. It consists of 2 tables "cast", "movie" and one-to-one relation "movie_cast". When I try to build a project the following errors occur:
error: Entities cannot have relations.
public final class MovieWithCastDbModel {
error: An entity must have at least 1 field annotated with #PrimaryKey
public final class MovieWithCastDbModel {
When I remove #Entity annotation from MovieWithCastDbModel class I get the following
error: Entity class must be annotated with #Entity
public final class MovieWithCastDbModel {
So whatever I do it tells me to do the opposite thing.
Here are the data classes themselves:
#Entity(tableName = "cast")
data class CastDbModel(
#PrimaryKey(autoGenerate = false)
var id : Int,
var name: String,
var profile_path: String,
var character: String)
#Entity(tableName = "movie")
#TypeConverters(IntConverter::class)
data class MovieDbModel(
var page: Int,
#PrimaryKey(autoGenerate = false)
var id: Int,
var poster_path: String,
var overview: String,
var title: String,
#ColumnInfo(name = "genre_ids")
var genre_ids: Genres,
var runtime: Int)
#Entity(tableName = "movie_cast")
class MovieWithCastDbModel(
#Embedded
var movie: MovieDbModel,
#Relation(
entity = CastDbModel::class,
parentColumn = "id",
entityColumn = "id"
)
var cast : CastDbModel
)
data class Genres(
#Embedded
var genre_ids: Int
)
class IntConverter {
#TypeConverter
fun fromGenres(value: Genres): String {
return Gson().toJson(value)
}
#TypeConverter
fun toGenres(value: String): Genres {
return Gson().fromJson(value,Genres::class.java)
}
}
What could be a problem?
As the error message says. Entities cannot have #Relation annotation.
A relation is not actually defined within a table (#Entity annotated class), they are independent unless you say otherwise by using Foreign Key constraints to enforce referential integrity (that children must have a parent).
Rather if you want to use a relationship, then you create a POJO class that can combine the data of the related tables.
What you have is a Cast table and a Movie table. The MovieCast table appears to be what is called an associative table (mapping table, reference table and other names). It caters for a many-many relationship. That is a Movie can have many related casts and a cast can be related to many Movies.
So for MovieWithCastDbModel you want something like:-
#Entity(
tableName = "movie_cast",
primaryKeys = ["movieIdMap","castIdMap"],
/* Optional Foreign Key constraints */
foreignKeys = [
/* A MovieId MUST be a value of an existing id column in the movie table */
ForeignKey(
entity = MovieDbModel::class,
parentColumns = ["id"],
childColumns = ["movieIdMap"],
/* Optional (helps maintain referential integrity) */
/* if parent is deleted then children rows of that parent are deleted */
onDelete = ForeignKey.CASCADE,
/* if parent column is changed then the column that references the parent is changed to the same value */
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = CastDbModel::class,
parentColumns = ["id"],
childColumns = ["castIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class MovieWithCastDbModel(
var movieIdMap: Int,
#ColumnInfo(index = true)
var castIdMap: Int
)
probably better to call it MovieCast or MoviesCastMap rather than MovieWithCast.
Now to allow MoviesWithCast's to be extracted you then have a POJO using #Embedded for the MovieDbModel and an #Relation that defines the association table.
So something like:-
data class MovieWithListOfCast(
#Embedded /* The parent */
var movie: MovieDbModel,
#Relation(
entity = CastDbModel::class, /* The class of the related table(entity) (the children)*/
parentColumn = "id", /* The column in the #Embedded class (parent) that is referenced/mapped to */
entityColumn = "id", /* The column in the #Relation class (child) that is referenced (many-many) or references the parent (one(parent)-many(children)) */
/* For the mapping table */
associateBy = Junction(
value = MovieWithCastDbModel::class, /* The class of the mapping table */
parentColumn = "movieIdMap", /* the column in the mapping table that maps/references the parent (#Embedded) */
entityColumn = "castIdMap" /* the column in the mapping table that maps/references the child (#Relation) */
)
)
var castList: List<CastDbModel>
)
If you then have a function in an #Dao annotated interface/asbtract class such as
#Transaction
#Query("SELECT * FROM movie")
fun getAllMoviesWithCastList(): List<MovieWithListOfCast>
The it will retrieve all the Movies with the related cast, as a list of MovieWithListOfCast objects (one per movie).
Room uses the annotations within the MovieWithListOfCast to extract ALL of the related cast. It does this with an underlying query run for each Movie and hence why #transaction should be used.
Related
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 have the following entity:
#Entity(tableName = "match_frames_table")
data class DbFrame(
#PrimaryKey(autoGenerate = false)
val frameId: Int
)
And below a data class with reference to 3 other entities, including a nested one, DbBreakWithPots:
data class DbFrameWithScoreAndBreakWithPotsAndBallStack(
#Embedded val frame: DbFrame,
#Relation(
parentColumn = "frameId",
entityColumn = "frameId",
)
val frameScore: List<DbScore>,
#Relation(
parentColumn = "frameId",
entityColumn = "frameId",
entity = DbBreak::class
)
val frameStack: List<DbBreakWithPots>,
#Relation(
parentColumn = "frameId",
entityColumn = "frameId"
)
val ballStack: List<DbBall>
)
Where DbBreakWithPots is as follows:
data class DbBreakWithPots(
#Embedded val matchBreak: DbBreak,
#Relation(
parentColumn = "breakId",
entityColumn = "breakId"
)
val matchPots: List<DbPot>
)
In my DAO, I implemented the query method, which works fine:
#Query("SELECT * FROM match_frames_table")
fun getMatchFrames(): LiveData<List<DbFrameWithScoreAndBreakWithPotsAndBallStack>>
However, at the moment I am inserting and deleting from the database manually table by table, but the fact that I have nested relations makes it tricky. Is there a way to simply insert a DbFrame and delete it through one sql request?
Let the CASCADE option of the onUpdate and onDelete actions do the work.
More specially define Foreign Keys in the child entities e.g. :-
#Entity(
foreignKeys = [
ForeignKey(entity = DBIdTotal::class, // Parent Entity
parentColumns = ["id_DBIdTotal"], // column(s) in the parent
childColumns = ["ref_DBIdTotal"], // column(s) in this table (the child)
onDelete = ForeignKey.CASCADE, //<<<<<<<<<< if parent is deleted, the deletion is cascaded to the respective children and they are updated
onUpdate = ForeignKey.CASCADE //<<<<<<<<<< if parent's referenced column(s) is updated then the updated value is changed in the respective children
)
]
)
data class DBIdOpPedido(
#PrimaryKey
val id_DBIdOpPedido: Long,
val ref_DBIdTotal: Long,
val op: String
)
You don't have to have both actions coded, you may wish to only use onDelete.
You may wish to look at:-
Foreign Key
SQLite - Foreign Key Support - ON DELETE and ON UPDATE Actions
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
The original question is at the very bottom. I have created a minimal (non-)working example of my problem which is hopefully easier to read through. The example is here at gitlab. There is a readme describing the problem. I am pasting some parts of the project here.
The data model is plain simple:
Owner <--(1:N)-- Child --(N:1)--> ReferencedByChild
All I want to do is to read an Owner from the database with all its associated Child objects and for each Child object
also the ReferencedByChild object that it references.
The whole code that reproduces my problem is below. What I am not 100% sure about is the #Relation on the OwnerWithEverything POJO. See below please.
#Database(
entities = [
Owner::class,
Child::class,
ReferencedByChild::class
],
version = 1
)
abstract class AppDatabase : RoomDatabase() {
abstract fun appDao(): AppDao
}
#Dao
abstract class AppDao {
#Insert
abstract fun insertOwner(owner: Owner): Long
#Insert
abstract fun insertChild(child: Child): Long
#Insert
abstract fun insertReferencedByChild(referencedByChild: ReferencedByChild): Long
#Query("SELECT * FROM Child INNER JOIN ReferencedByChild ON Child.referencedByChildId = ReferencedByChild.refByChildId ORDER BY Child.childText")
abstract fun findAllChildrenWithReferencedClasses(): List<ChildWithReferenced>
// Commenting this query out makes the build pass, so something here is incorrect.
#Query("SELECT * FROM Owner")
abstract fun findOwnersWithEverything(): List<OwnerWithEverything>
}
// ENTITIES
#Entity
data class Owner(
#PrimaryKey(autoGenerate = true)
val ownerId: Long,
val ownerText: String
)
#Entity(
foreignKeys = [
ForeignKey(
entity = Owner::class,
parentColumns = arrayOf("ownerId"),
childColumns = arrayOf("referencedOwnerId"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = ReferencedByChild::class,
parentColumns = arrayOf("refByChildId"),
childColumns = arrayOf("referencedByChildId"),
onDelete = ForeignKey.CASCADE
)
]
)
data class Child(
#PrimaryKey(autoGenerate = true)
val childId: Long,
val childText: String,
val referencedOwnerId: Long,
val referencedByChildId: Long
)
#Entity
data class ReferencedByChild(
#PrimaryKey(autoGenerate = true)
val refByChildId: Long,
val refText: String
)
// POJOS
// The Child has exactly one ReferencedByChild reference. This POJO joins those two
class ChildWithReferenced(
#Embedded
var child: Child,
#Embedded
var referencedByChild: ReferencedByChild
)
class OwnerWithEverything {
#Embedded
var owner: Owner? = null
#Relation(
parentColumn = "ownerId",
entityColumn = "referencedOwnerId",
entity = Child::class // which entity should be defined here?
)
var childrenWithReferenced: List<ChildWithReferenced>? = null
}
Building this code results in this error message:
error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such column: refByChildId)
I think that the Owner query is badly constructed, but I am not entirely sure. If that is the problem, what is the correct way to construct the query?
This is the original question
I have a nested POJO structure that should represent a single Game having multiple Rounds and each Round has a single Topic associated with it:
class GameWithRounds {
#Embedded
var game: Game? = null
#Relation(
parentColumn = "id",
entityColumn = "gameId",
entity = RoundRoom::class
)
var rounds: List<RoundWithTopic>? = null
}
class RoundWithTopic(
#Embedded
var round: RoundRoom,
#Embedded(prefix = "topic_")
var topic: Topic
)
The embedded annotation on Topic specifies a prefix because there are clashing id properties.
The Room Query that can fetch those classes is:
#Query("SELECT Topic.id as topic_id, Topic.name as topic_name, (...), RoundRoom.* FROM RoundRoom INNER JOIN Topic ON RoundRoom.topicId = Topic.id")
abstract fun findRoundsWithTopics(): List<RoundWithTopic>
However, building the project gives me Room errors:
There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such column: topic_id)
Even though when I induce a warning about which fields are actually present, this is what Room tells me:
Columns returned by the query: topic_id, topic_name, topic_description, topic_language, topic_keywords, topic_sourceUrls, topic_furtherUrls, topic_questions, order, gameId, topicId, status, id. Fields in cz.melkamar.sklapecka.model.RoundWithTopic: order, gameId, topicId, status, id, topic_id, topic_name, topic_description, topic_language, topic_keywords, topic_sourceUrls, topic_furtherUrls, topic_questions, topic_image.
The topic_id column is there in the query result! Why am I getting this error?
For completeness, this is the entities:
#Entity
data class Game(
#PrimaryKey(autoGenerate = true)
val id: Long = 0,
#Embedded
val gameConfigurationEmbed: GameConfigurationEmbed
)
data class GameConfigurationEmbed(
var secondsPerTurn: Int,
var maxSecondsPerTurn: Int,
var bonusSecondsPerAnswer: Int
)
#Entity(
foreignKeys = [
ForeignKey(
entity = Game::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("gameId"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Topic::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("topicId"),
onDelete = ForeignKey.CASCADE
)
]
)
#TypeConverters(RoomConverters::class)
data class RoundRoom(
val order: Int,
var gameId: Long,
val topicId: String,
var status: RoundStatus = RoundStatus.CREATED,
#PrimaryKey(autoGenerate = true)
val id: Long = 0
) {
enum class RoundStatus {
CREATED, UPCOMING, IN_PROGRESS, FINISHED
}
}
#Entity
data class Topic(
#PrimaryKey val id: String,
val name: String,
val description: String,
val language: String,
val keywords: List<String>,
val sourceUrls: List<String>,
val furtherUrls: List<String>,
val questions: List<String>,
val image: ByteArray?
)
After some research, specifically looking at this link : How can I represent a many to many relation with Android Room?
the only answers we found would be to
either create a hand-made sql query to handle this type of situation
where you have a many-to-many relationship
OR
to alternatively have an additional joining entity which gets
updated as the rest of the objects are updated. With this approach, you can get the ID's and then create additional queries as needed
It seems embedded fields and type converter is not properly used on observing the question. I don't want to go in detail in the solution of the question since it is trying to use complex relations and I cannot test it replicating in my machine.
But I want to provide insight on using Embedded fields and TypeConverter.
Let's take an example from the question above:
Game table has fields
id, secondsPerTurn, maxSecondsPerTurn, bonusSecondsPerAnswer.
It is okay to create entity like below.
#Entity
data class Game(
#PrimaryKey(autoGenerate = true)
val id: Long = 0,
#Embedded
val gameConfigurationEmbed: GameConfigurationEmbed
)
data class GameConfigurationEmbed(
var secondsPerTurn: Int,
var maxSecondsPerTurn: Int,
var bonusSecondsPerAnswer: Int
)
Here in the SQLite table, data is actually stored in four different columns but Room does CRUD operation based on data class structure giving higher feasibilty to the developers.
TypeConverter
Type converter will be helpful if we want to store non primitive data types or same type of data which will not be covered by #Embedded.
For example, a football game can be held in two places: home and away. Home and Away can have the same field names like placeName, latitude, longitude. In this case, we can create data class and type converters like below:
data class GamePlace(
val placeName:String,
val latitude:String,
val longitude:String
)
#Entity
data class Game(
#PrimaryKey(autoGenerate = true)
val id: Long = 0,
#Embedded
val gameConfigurationEmbed: GameConfigurationEmbed
#TypeConverters
var home: GamePlace? = null,
#TypeConverters
var away: GamePlace? = null,
)
object Converters {
private val gson = Gson()
#TypeConverter
#JvmStatic
fun fromGamePlace(gamePlace: GamePlace?): String? {
return if (gamePlace == null) null else gson.toJson(gamePlace)
}
#TypeConverter
#JvmStatic
fun toGamePlace(jsonData: String?): GamePlace? {
return if (jsonData.isNullOrEmpty()) null
else gson.fromJson(jsonData, object : TypeToken<GamePlace?>() {}.type)
}
}
While using type converter, converter class should be defined in database class as below:
#Database(
entities = [Game::class /* ,more classes here*/],
version = 1
)
#TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun gameDao(): GameDao
//.....
}
I hope this will help to deal with Embedded and TypeConverter in Room.
I have a situation, where I want to insert data in Master (store_master) and Mapping (store_mapping) table in one to many relation using Room persistence Library. My implementation is below:
#Entity(tableName = "store_master")
class Store {
#PrimaryKey(autoGenerate = true)
public var store_id: Int = 0
#SerializedName("name")
lateinit var store_name: String
}
#Entity(tableName = "store_mapping", foreignKeys = arrayOf(
ForeignKey(entity = Store::class,
parentColumns = arrayOf("store_id"),
childColumns = arrayOf("pic_id"),
onDelete = CASCADE)))
class StorePicture(#field:PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id") var id: Int,
#SerializedName("pic_id") var pic_id: Int?,
#SerializedName("image") var storage_picture: String?)
class StoreWithPictures {
#Embedded
var store: Store? = null
#Relation(parentColumn = "store_id",
entityColumn = "pic_id")
var pictures: List<StorePicture> = ArrayList()
}
For fetching Store with Pictures my implementation is below:
#Transaction
#Query("SELECT * FROM store_master ORDER BY store_master.storage_id DESC")
fun loadAll(): List<StoreWithPictures>
Above approach works fine for fetching data from master and mapping table, but I'm not able to insert in same fashion (i.e using #Embeded and #transaction annotations).
I have fixed this issue with #Transaction annotations.
#Transaction
fun insertStoreWithPictures(store: Store, pictures: List<StorePicture>) {
insertStore(store)
insertPictures(pictures)
}
Annotating a method with #Transaction makes sure that all database
operations you’re executing in that method will be run inside one
transaction. The transaction will fail when an exception is thrown in
the method body.