Insert nested object in multiple tables using Room - android

I have a Json similar to below format:
{
"gender": "male",
"name": {
"first": "Axel",
"last": "Raab"
}
}
I want to create 2 tables, say Person and Name and store this data in these 2 tables.
My Entity classes look like these:
#Entity
data class Person(
#PrimaryKey(autoGenerate = true)
var pid: Int,
#SerializedName("gender")
var gender: String?,
#SerializedName("name")
#Ignore
var name: Name?,
)
#Entity(indices = [Index("pId")],
foreignKeys = [ForeignKey(
entity = Person::class,
parentColumns = ["pid"],
childColumns = ["pId"],
onDelete = ForeignKey.CASCADE)])
data class Name(
#PrimaryKey(autoGenerate = true)
val mId: Int,
#SerializedName("first")
var first: String?,
#SerializedName("last")
var last: String?,
var pId: Int
)
I have created corresponding Daos but unable to identify a way where I can write a transaction that would store this data into 2 tables and relate then using provided ids.
I have gone through many solutions that would either use #Embedded or #Relation or TypeConverter approaches but either they do not fit my use case or the solution is about querying the data and not how to save or insert.
Thanks.

You can use Embedded
e.g
public class Coordinates {
double latitude;
double longitude;
}
public class Address {
String street;
#Embedded
Coordinates coordinates;
}
Insert example in your DAO
#Insert
fun insert(item: Coordinates)
you can pass Coordinates instance variable in your insert function
Address("your street", Coordinates(your_lat,your_lng))

Related

Cannot build relations in Room database

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.

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

Retrofit to Room models mapping

I have a question that maybe will be easy for some of you but I really can't solve.
I have a json formatted like this:
{
"id" : "1641S818",
"balance" : "100.20",
"transactions" : [
{
"id" : "item1",
"price" : "1.50",
"description" : "pen",
"date" : "2018-05-14T14:19:00Z"
},
{
"id" : "item1",
"price" : "9.00",
"description" : "book",
"date" : "2018-05-14T08:19:00Z"
}
]
}
I wanted to try and put up an app with Retrofit + Room for the first time, and I'm having trouble building models.
I came up with this data classes for Retrofit and they work fine. This is no surprise as I know how to use it.
data class Account (
val id : String,
val balance : Double,
val transactions : List<Transaction>
)
data class Transaction (
val id : String,
val price : Double,
val description : String,
val date : String
)
The real issues start when I want to save this structure in a database, with this one-to-many relationship existing between the Account and the Transaction. So far I understood that Room cannot handle this type of structure and I should build different classes for the database model. So I did:
#Entity(tableName = "account")
data class AccountData(
#ColumnInfo(name = "id")
#PrimaryKey(autoGenerate = false)
val id: String,
val balance: Double,
)
#Entity(
tableName = "transaction",
foreignKeys = [
ForeignKey(
entity = AccountData::class,
parentColumns = ["id"],
childColumns = ["account_id"],
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)
]
)
data class TransactionData(
#PrimaryKey(autoGenerate = false)
#ColumnInfo(name = "id")
val transactionId: String,
#ColumnInfo(name = "account_id")
val accountId: String,
val price: Double,
val description: String,
val date: String
)
Now the part where I'm stuck. I cannot find a guide, an example, anything that shows where to convert the models with a mapper (maybe using livedata or rx) with this particular case, which is when we have complex relations between objects and not just plain classes.
You can define separate Mapper file with extension functions like this
fun Transaction.toTransactionData() = TransactionData(...)
And then after API call, probably in UseCase, you can use this function to map your API entity to DB entity and then pass result to Room dao.
UPD 1.
Additionally you can define Transation entity for UI usage. So at UseCase level you operate with RestTransaction and DbTransaction, but passing Transaction to UI, to abstract from its source.

"no such column" SQL error from Android Room and multiple #Embedded fields on POJO

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.

Insert One to Many Relationship using Room Persistence Library

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.

Categories

Resources