Related
I have five tables in my database: AREA, AREA_TYPE, SAMPLE, PACK, UNIT
#Entity(tableName = "AREA")
data class AreaEntity(
#PrimaryKey val id:String,
val title:String,
#ColumnInfo(name = "area_type_id") val areaTypeId:Int,
#ColumnInfo(name = "is_active") val isActive:Boolean
)
#Entity(tableName = "AREA_TYPE")
data class AreaTypeEntity(
#PrimaryKey val id:String,
val title:String,
#ColumnInfo(name = "title") val parentAreaId : String
)
#Entity(tableName = "SAMPLE")
data class SampleEntity(
#PrimaryKey val id:String,
val title:String,
)
#Entity(tableName = "PACK")
data class PackEntity(
#PrimaryKey val id:String,
val title:String,
)
#Entity(tableName = "UNIT")
data class UnitEntity(
#PrimaryKey val id:String,
#ColumnInfo(name = "sample_id") val parentAreaId : String,
#ColumnInfo(name = "area_id") val areaId:Int,
#ColumnInfo(name = "pack_type_id") val packTypeId: Int,
#ColumnInfo(name = "is_active") val isActive:Boolean
)
UNIT table has three foreign keys : sample_id, area_id, pack_id
Every area has one-to-one relationship with area type.
I have an AreaPOJO for Area-AreaType Relationship:
data class AreaPOJO (
#Embedded val areaEntity : AreaEntity
#Relation (
parentColumn = "area_id",
entityColumn = "id"
)
val areaTypeEntity : AreaTypeEntity
)
Visual view of tables (https://i.stack.imgur.com/bXzl5.png)
So I assume that I will have a POJO for UNIT for the Relationships like this:
data class UnitPOJO (
#Embedded val unitEntity : UnitEntity
#Relation (
parentColumn = "area_id",
entityColumn = "id"
)
val areaEntity : AreaEntity
#Relation (
parentColumn = "pack_id",
entityColumn = "id"
)
val packEntity : PackEntity
#Relation (
parentColumn = "sample_id",
entityColumn = "id"
)
val sampleEntity : SampleEntity
)
With this POJO, I can get AreaEntity,SampleEntity,UnitEntity but I can't get AreaTypeEntity for UnitPOJO.
When I use AreaPOJO instead of AreaEntity, I have a compilation error which tells me to use "prefix" for AreaPOJO. When I use prefix, this time AreaPOJO gives an error that it can't find the column names for relationship.
So I am stuck :)
Briefly I need all the fields from all five tables for this query :
"SELECT * FROM UNIT
INNER JOIN AREA ON UNIT.AREA_ID = AREA.ID
INNER JOIN AREA_TYPE ON AREA.AREA_TYPE_ID = AREA_TYPE.ID
INNER JOIN SAMPLE ON UNIT.SAMPLE_ID = SAMPLE.ID
INNER JOIN PACK ON UNIT.PACK_ID = PACK.ID"
First the use of prefix, this is an option to circumvent the ambiguous column names (e.g. which id column is the correct one to use? (rhetorical)) BUT you would then have to play around with the queries to include AS (implicitly or explicitly) to rename the extracted columns.
I would suggest that using unique column names is the way to avoid such ambiguities.
Onto the grandparent/grandchild.
In short you are close BUT you retrieve an AreaPOJO (Area with Type) not an AreaEntity, but you then have to tell Room to use the AreaEntity class (as that is the class used to ascertain the columns for the AreaEntity and then the #relation in the AreaPOJO knows to get the inderlying AreaType).
So although untested but successfully compiled consider the following:-
#Entity(tableName = "UNIT")
data class UnitEntity(
#PrimaryKey val id:String,
#ColumnInfo(name = "sample_id") val parentAreaId : String,
#ColumnInfo(name = "area_id") val areaId:Int,
#ColumnInfo(name = "pack_type_id") val packTypeId: Int,
#ColumnInfo(name = "is_active") val isActive:Boolean
)
#Entity(tableName = "AREA_TYPE")
data class AreaTypeEntity(
#PrimaryKey #ColumnInfo(name = "area_type_id") val id:String, //<<<<< unique name
val title:String,
#ColumnInfo(name = "area_type_title") val parentAreaId : String
)
data class AreaPOJO(
#Embedded val areaEntity : AreaEntity,
#Relation(
parentColumn = "area_type_id", //<<<<< changed accrodingly
entityColumn = "area_type_id" //<<<<< changed accordingly
)
val areaTypeEntity : AreaTypeEntity
)
data class UnitPOJO (
#Embedded val unitEntity : UnitEntity,
#Relation (
entity = AreaEntity::class, //<<<<< ADDED
parentColumn = "area_id",
entityColumn = "area_id"
)
val areaWithAreaType : AreaPOJO,
#Relation (
parentColumn = "pack_type_id",
entityColumn = "pack_id"
)
val packEntity : PackEntity,
#Relation (
parentColumn = "sample_id",
entityColumn = "sample_id"
)
val sampleEntity : SampleEntity
)
Notice the pack_type_id and pack_id in the UnitPOJO as opposed to sample_id and sample_id for the sample reference/relationship.
I would suggest considering using unique names e.g. pack_type_id is referencing/mapping the relationship between the unit and the pack, perhaps naming the column pack_id_map in the Unit. Thus the column names are then more descriptive and also unique. The downside is that there is more coding.
using the above the #Query could be :-
#Transaction
#Query("SELECT * FROM UNIT")
fun getUnitsWithRelations(): List<UnitPOJO>
Obviously adjusted according if using Flow/Live Data and so on.
Saying that, the above is inefficient as when Room processes an #Relation it builds and underlying query per #Relation that gets ALL the children from the parent (I believe on a per parent basis). In your case, it appears that you have 1 to many relationships thus #Embedded can be used BUT the query has to be more complex.
Working Example
The following is a working example based upon your code that
uses both #Relation and #Embedded resolutions
The #Relation POJO's are UnitPOJO and AreaPOJO
The #Embedded versions are prefixed with Alternative
Adds data (3 rows into each table, except 5 Units)
Extracts the Unit's and the related data using both alternatives
includes Foreign Key constraints that enforce referential and maintain integrity see https://sqlite.org/foreignkeys.html
It should be noted that some changes have been made as I believe that you some unusual and at a guess unecesassry relationships. e.g. You appear to have Area relate to AreaType both ways when only one is required. That is an Area will have an AreaType as a parent but if an ArearType also has an Area as a parent then you get the chicken and egg scenario.
The assumption has been made that an Area has one of the many available AreaTypes as a parent.
First the classes (see comments) :-
#Entity(
tableName = "AREA",
/* Enforces/Maintains referential Integrity */
/* i.e does not allow orphans */
foreignKeys = [
ForeignKey(
entity = AreaTypeEntity::class,
parentColumns = ["area_type_id"],
childColumns = ["area_type_id_map" ],
onDelete = ForeignKey.CASCADE /* ????? */,
onUpdate = ForeignKey.CASCADE /* ????? */
)
]
)
data class AreaEntity(
#PrimaryKey #ColumnInfo(name = "area_id")val id:String, //<<<<< unique name
#ColumnInfo(name = "area_title") val title:String,
#ColumnInfo(name = "area_type_id_map") val areaTypeId:String, //<<<<< see Area Type
#ColumnInfo(name = "area_is_active") val isActive:Boolean
)
#Entity(tableName = "SAMPLE")
data class SampleEntity(
#PrimaryKey #ColumnInfo(name = "sample_id") val id:String, //<<<<< unique name
#ColumnInfo(name = "sample_title") val title:String,
)
#Entity(tableName = "PACK")
data class PackEntity(
#PrimaryKey #ColumnInfo(name = "pack_id") val id:String, //<<<<< unique name
#ColumnInfo(name = "pack_title") val title:String, //<<<<< unique name
)
#Entity(
tableName = "UNIT",
foreignKeys = [
ForeignKey(
entity = SampleEntity::class,
parentColumns = ["sample_id"],
childColumns = ["sample_id_map"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = AreaEntity::class,
parentColumns = ["area_id"],
childColumns = ["area_id_map"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = PackEntity::class,
parentColumns = ["pack_id"],
childColumns = ["pack_id_map"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class UnitEntity(
#PrimaryKey val id:String,
#ColumnInfo(name = "sample_id_map") val sampleId : String,
#ColumnInfo(name = "area_id_map") val areaId:String,
#ColumnInfo(name = "pack_id_map") val packTypeId: String,
#ColumnInfo(name = "unit_is_active") val isActive:Boolean
)
#Entity(
tableName = "AREA_TYPE"
)
data class AreaTypeEntity(
#PrimaryKey #ColumnInfo(name = "area_type_id") val id:String, //<<<<< unique name
#ColumnInfo(name = "area_type_title") val title:String,
/* ???? should an area type have an area as a parent? potential issues if so */
/* commented out
#ColumnInfo(name = "area_type_title") val parentAreaId : String //<<<<< unique name
*/
)
data class AreaPOJO(
#Embedded val areaEntity : AreaEntity,
#Relation(
parentColumn = "area_type_id_map", //<<<<< changed accordingly
entityColumn = "area_type_id" //<<<<< changed accordingly
)
val areaTypeEntity : AreaTypeEntity
)
data class UnitPOJO (
#Embedded val unitEntity : UnitEntity,
#Relation (
entity = AreaEntity::class, //<<<<< ADDED
parentColumn = "area_id_map",
entityColumn = "area_id"
)
val areaWithAreaType : AreaPOJO,
#Relation (
parentColumn = "pack_id_map",
entityColumn = "pack_id"
)
val packEntity : PackEntity,
#Relation (
parentColumn = "sample_id_map",
entityColumn = "sample_id"
)
val sampleEntity : SampleEntity
)
data class AlternativeAreaPOJO (
#Embedded val areaEntity: AreaEntity,
#Embedded val areaTypeEntity: AreaTypeEntity
)
data class AlternativeUnitPOJO (
#Embedded val unitEntity: UnitEntity,
#Embedded val alternativeAreaPOJO: AlternativeAreaPOJO,
#Embedded val packEntity: PackEntity,
#Embedded val sampleEntity: SampleEntity
)
The #Dao annotated interface AllDao :-
#Dao
interface AllDAO {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(areaEntity: AreaEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(unitEntity: UnitEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(sampleEntity: SampleEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(packEntity: PackEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(areaTypeEntity: AreaTypeEntity)
#Transaction
#Query("SELECT * FROM UNIT")
fun getUnitsWithRelations(): List<UnitPOJO>
#Query("SELECT * FROM UNIT " +
"INNER JOIN AREA ON UNIT.area_id_map = AREA.area_id " +
"INNER JOIN AREA_TYPE ON AREA.area_type_id_map = AREA_TYPE.area_type_id " +
"INNER JOIN SAMPLE ON UNIT.sample_id_map = SAMPLE.sample_id " +
"INNER JOIN PACK ON UNIT.pack_id_map = PACK.pack_id")
fun getAlternativeUnitsWithRelations(): List<AlternativeUnitPOJO>
}
The #Database annotated class TheDatabase :-
#Database(entities = [
AreaEntity::class,
SampleEntity::class,
PackEntity::class,
UnitEntity::class,
AreaTypeEntity::class
],
version = 1,
exportSchema = false
)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDAO(): AllDAO
companion object {
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
context,
TheDatabase::class.java,
"the_database.db"
)
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
Note for convenience and brevity .allowMainThreadQueries has been utilised.
Code within an activity (designed to run just the once):-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDAO()
val TAG = "DBINFO"
val p1 = PackEntity("P001","Pack1")
val p2 = PackEntity("P002","Pack2")
val p3 = PackEntity("P003","Pack3")
dao.insert(p1)
dao.insert(p2)
dao.insert(p3)
val s1 = SampleEntity("S001","Sample1")
val s2 = SampleEntity("S002","Sample2")
val s3 = SampleEntity("S003","Sample3")
dao.insert(s1)
dao.insert(s2)
dao.insert(s3)
val at1 = AreaTypeEntity("AT001","AreaType1")
val at2 = AreaTypeEntity("AT002","AreaType2")
val at3 = AreaTypeEntity("AT003","AreaType3",)
dao.insert(at1)
dao.insert(at2)
dao.insert(at3)
val a1 = AreaEntity("A001","Area1",at1.id,true)
val a2 = AreaEntity("A002","Area2",at2.id,false)
val a3 = AreaEntity("A003","Area3",at1.id,true)
dao.insert(a1)
dao.insert(a2)
dao.insert(a3)
dao.insert(UnitEntity("U001",s1.id,a1.id,p1.id,true))
dao.insert(UnitEntity("U002",s2.id,a2.id,p2.id, false))
dao.insert(UnitEntity("U003",s3.id,a3.id,p3.id,true))
dao.insert(UnitEntity("U004",s1.id,a2.id,p3.id,false))
dao.insert(UnitEntity("U005",s3.id,a2.id,p1.id, true))
for(uwr in dao.getUnitsWithRelations()) {
Log.d(TAG,
"Unit is ${uwr.unitEntity.id} " +
"Active = ${uwr.unitEntity.isActive} " +
"Sample is ${uwr.sampleEntity.title} " +
"Area is ${uwr.areaWithAreaType.areaEntity.title} " +
"AreaType is ${uwr.areaWithAreaType.areaTypeEntity.title}"
)
}
for (auwr in dao.getAlternativeUnitsWithRelations()) {
Log.d(TAG,
"Unit is ${auwr.unitEntity.id} " +
"Active is ${auwr.unitEntity.isActive} " +
"Sample is ${auwr.sampleEntity.title} " +
"Area is ${auwr.alternativeAreaPOJO.areaEntity.title} " +
"AreaType is ${auwr.alternativeAreaPOJO.areaTypeEntity.title}"
)
}
}
}
Last the resultant output from the log:-
2022-04-05 09:32:40.528 D/DBINFO: Unit is U001 Active = true Sample is Sample1 Area is Area1 AreaType is AreaType1
2022-04-05 09:32:40.528 D/DBINFO: Unit is U002 Active = false Sample is Sample2 Area is Area2 AreaType is AreaType2
2022-04-05 09:32:40.529 D/DBINFO: Unit is U003 Active = true Sample is Sample3 Area is Area3 AreaType is AreaType1
2022-04-05 09:32:40.529 D/DBINFO: Unit is U004 Active = false Sample is Sample1 Area is Area2 AreaType is AreaType2
2022-04-05 09:32:40.529 D/DBINFO: Unit is U005 Active = true Sample is Sample3 Area is Area2 AreaType is AreaType2
2022-04-05 09:32:40.537 D/DBINFO: Unit is U001 Active is true Sample is Sample1 Area is Area1 AreaType is AreaType1
2022-04-05 09:32:40.537 D/DBINFO: Unit is U002 Active is false Sample is Sample2 Area is Area2 AreaType is AreaType2
2022-04-05 09:32:40.537 D/DBINFO: Unit is U003 Active is true Sample is Sample3 Area is Area3 AreaType is AreaType1
2022-04-05 09:32:40.537 D/DBINFO: Unit is U004 Active is false Sample is Sample1 Area is Area2 AreaType is AreaType2
2022-04-05 09:32:40.537 D/DBINFO: Unit is U005 Active is true Sample is Sample3 Area is Area2 AreaType is AreaType2
i.e. the results are the same for the 2 alternatives and of course the relationships are working as expected
Actually, I have solved it by this POJO :
data class UnitPOJO
(
#Embedded val unit: UnitEntity,
#Relation(
parentColumn = "sample_id",
entityColumn = "id"
)
val sampleEntity: SampleEntity,
#Relation(
parentColumn = "pack_type_id",
entityColumn = "id"
)
val pack: PackEntity,
#Relation(
parentColumn = "area_id",
entityColumn = "id",
entity = AreaEntity::class
)
val area: AreaPOJO
)
and AreaPOJO like this :
data class AreaPOJO(
#Embedded val areaEntity: AreaEntity,
#Relation(
parentColumn = "area_type_id",
entityColumn = "id"
)
val areaTypeEntity: AreaTypeEntity
)
But I will definitely consider your warnings #MikeT about naming the fields/column names when using hierarchical data. If something goes wrong or I get unexpected results, I will definitely use this approach to solve the problem.
So I am build an app where I want to store information coming from various source into a single database. The database is as below:
#Entity(tableName = "store")
data class StoreEntity (
#PrimaryKey()
#ColumnInfo(name = "")
var current: String,
#ColumnInfo(name = "ingredientslist")
var productsIngredientList: MutableList<IngredientsEntity?>,
#ColumnInfo(name = "reviewslist")
var productsReviewsList: MutableList<ReviewsEntity?>,
#ColumnInfo(name = "listofproductsId")
var listOfId: MutableList<String>
)
and the other entities are:
#Entity(tableName = "ingredients")
data class IngredientsEntity(
#PrimaryKey()
#NonNull var id: String,
#ColumnInfo(name = "ingredient")
var ingredient: Ingredient? = null,
)
#Entity(tableName = "reviews")
data class IngredientsEntity(
#PrimaryKey()
#NonNull var id: String,
#ColumnInfo(name = "review")
var review: Review? = null,
)
I have defined the Doa as below:
#Query("UPDATE store SELECT ingredientslist SET ingredient=:shadow WHERE id = :id ")
suspend fun updateIngredients(id: String?, ingredient: Ingredient)
#Query("SELECT ingredient from ingredientslist WHERE id = :id ")
suspend fun getIngredients(id: String): Flow<Ingredient?>
suspend fun getIngredientsDistinctUntilChanged(id: String) = getIngredients(id).distinctUntilChanged()
reviews and ingredients are both coming from different sources but I would like a database where I can store :
current product displayed -> String (only one)
list of products -> list of String
list of Ingredients (List of id + ingredients)
list of Reviews (List of id + reviews)
the goal would be to be able to add/retrieve ingredients using id and same for reviews.
I am not sure if I have to access using the Doa the store database, then select ingrediendslist, then look for ingredients link to an id. or if I can just access directky ingrediendslist because store is automatically linking ingredientslist
Also is there a way to have a single PrimaryKey, which could be a single name ? the Store database will only have one single entry. See this like you can have only one store.
It's look likes to me a database into a database.
Any idea how to make it works ? I tried several Room sql command but I am not sure that it's the right way.
Maybe I need to split it into different Dbs. One for ingredients, one for reviews and one for current product id and list of products ids.
I was trying to do it in one single database to avoid having multiple DBs for only 15 products top.
Any idea or advices ?
Thanks
The database is as below:
That would be in theory, there are various issues that would result in the compilation failing. Such as trying to give a column no name as per :-
#Entity(tableName = "store")
data class StoreEntity (
#PrimaryKey()
#ColumnInfo(name = "")
var current: String,
Furthermore to store a list of Objects, although possible with the use of TypeConverters restricts or complicates matters with regard to best use of the database. Doing so also de-normalises the database and adds bloat as an object if referred to multiple times (such as an ingredient which may in theory be common to a number of stores).
Complexity arises through a value of any such object being stored with all the other values and thus would require complex SQL very likely including CASE THEN ELSE END constructs within and possibly Common Table Expressions.
As you are in the development stage I would suggest consider using the power of relationships basically have an extra 2 tables one to map/reference/associate a Store with it's ingredients, another to map a Store with it's Reviews.
Here's a working example (without products but the same technique applies for products)
So first the StoreEntity :-
#Entity(tableName = "store")
data class StoreEntity (
#PrimaryKey()
#ColumnInfo(name = "store_identifier")
var current: String
/* lists are mapped so not needed here (see StoreWithMappedReviewsAndWithMappedIngredients)
#ColumnInfo(name = "ingredientslist")
var productsIngredientList: MutableList<IngredientsEntity?>,
#ColumnInfo(name = "reviewslist")
var productsReviewsList: MutableList<ReviewsEntity?>,
#ColumnInfo(name = "listofproductsId")
var listOfId: MutableList<String>
*/
)
The Review class (made up as not included) :-
data class Review(
var reviewTitle: String,
var reviewer: String
/* etc */
)
And like your code the respective ReviewsEnntity (that Embeds (copies the member variables) the underlying Review) :-
#Entity(tableName = "reviews")
data class /*IngredientsEntity????*/ ReviewsEntity(
#PrimaryKey()
#NonNull var reviewId: String,
/* see comments for Ingredient class
#ColumnInfo(name = "review")
var review: /*Reviews? ????*/ Review = null,
*/
#Embedded
var review: Review
)
Similar for Ingredient (made up again) and IngredientsEntity :-
data class Ingredient(
var ingredientName: String,
var ingredientDescription: String
/* other member variables as required */
)
and :-
#Entity(tableName = "ingredients")
data class IngredientsEntity(
#PrimaryKey
#NonNull var ingredientId: String,
#Embedded
var ingredient: Ingredient
/* using below would require a TypeConverter the above embeds the ingredient so a column as per the ingredient class */
/*var ingredient: Ingredient? = null <<<<< null ???? why have an ingredient row with no ingredient? */
)
Now the two (3rd for products) mapping/associate/reference .... tables.
First the table that maps Stores and Reviews with the potential for many stores to have many Reviews and for Reviews to map to many Stores.
StoreReviewMappingEntity
#Entity(
tableName = "store_review_map",
primaryKeys = ["storeReviewMap_storeId","storeReviewMap_reviewId"]
/* Optional Foreign Key Constraints to enforce Referential Integrity */
)
data class StoreReviewMappingEntity(
var storeReviewMap_storeId: String,
#ColumnInfo(index = true)
var storeReviewMap_reviewId: String
)
and StoreIngredientMappingEntity for Ingredients :-
#Entity(
tableName = "store_ingredient_map",
primaryKeys = ["storeIngredientMap_storeId","storeIngredientMap_ingredientId"]
/* Optional Foreign Key Constraints to enforce Referential Integrity */
)
data class StoreIngredientMappingEntity(
var storeIngredientMap_storeId: String,
#ColumnInfo(index = true) /* index on the ingredient id so getting the ingredient is more efficient */
var storeIngredientMap_ingredientId: String
)
So instead of the 3 tables 5 tables (entities)
Now a POJO (i.e. not an Entity) StoreWithMappedReviewsAndWithMappedIngredients that allows you to get a Store with all of it's Reviews and all of it's ingredients via the mapping tables:-
data class StoreWithMappedReviewsAndWithMappedIngredients(
#Embedded
var storeEntity: StoreEntity,
#Relation(
entity = ReviewsEntity::class,
parentColumn = "store_identifier",
entityColumn = "reviewId",
associateBy = Junction(
value = StoreReviewMappingEntity::class /* the mappping table class/entity */,
parentColumn = "storeReviewMap_storeId",
entityColumn = "storeReviewMap_reviewId"
)
)
var reviewList: List<ReviewsEntity>,
#Relation(
entity = IngredientsEntity::class,
parentColumn = "store_identifier",
entityColumn = "ingredientId",
associateBy = Junction(
value = StoreIngredientMappingEntity::class,
parentColumn = "storeIngredientMap_storeId",
entityColumn = "storeIngredientMap_ingredientId"
)
)
var ingredientList: List<IngredientsEntity>
)
Now the #Dao annotated interface (one for brevity/convenience) AllDao :-
#Dao
interface AllDao {
/*
#Query("UPDATE store SELECT ingredientslist SET ingredient=:shadow WHERE id = :id ")
suspend fun updateIngredients(id: String?, ingredient: Ingredient)
#Query("SELECT ingredient from ingredientslist WHERE id = :id ")
suspend fun getIngredients(id: String): Flow<Ingredient?>
suspend fun getIngredientsDistinctUntilChanged(id: String) = getIngredients(id).distinctUntilChanged()
*/
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(ingredientsEntity: IngredientsEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(reviewsEntity: ReviewsEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(storeEntity: StoreEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(storeReviewMappingEntity: StoreReviewMappingEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(storeIngredientMappingEntity: StoreIngredientMappingEntity): Long
/* Allows removal of Review/Ingredient respectively (if only one store then no need for storeId but just-in-case) */
#Query("DELETE FROM store_review_map WHERE storeReviewMap_ReviewId=:reviewId AND storeReviewMap_StoreId=:storeId")
fun deleteReviewFromStore(storeId: String, reviewId: String): Int
#Query("DELETE FROM store_ingredient_map WHERE storeIngredientMap_IngredientId=:ingredientId AND storeIngredientMap_StoreId=:storeId")
fun deleteIngredientFromStore(storeId: String, ingredientId: String): Int
#Transaction
#Query("SELECT * FROM store")
fun getAllStoresWithReviewsAndWithIngredients(): List<StoreWithMappedReviewsAndWithMappedIngredients>
}
Note for the example .allowMainThreadQueries has been used so suspend not used just add as appropriate.
Nearly there here's an #Database annotated class :-
#Database(entities = [
StoreEntity::class,
ReviewsEntity::class,
IngredientsEntity::class,
StoreReviewMappingEntity::class,
StoreIngredientMappingEntity::class
],
version = 1,
exportSchema = false /* consider true see https://developer.android.com/training/data-storage/room/migrating-db-versions */
)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
#Volatile
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
context,
TheDatabase::class.java,
"the_database.db"
)
.allowMainThreadQueries() /* for convenience/brevity of the example */
.build()
}
return instance as TheDatabase
}
}
}
Finally putting it al together in an Activity which inserts 2 Stores, 4 reviwa, 4 Ingredients and maps reviews and ingredients to only the first Store and finally extracts all of the Stores with the Reviews and Ingredients and writes the result to the log. :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
val store1Id = "S001"
val store2Id = "S002"
val store1 = dao.insert(StoreEntity(store1Id))
val store2 = dao.insert(StoreEntity(store2Id))
val mercuryId = "I001"
val silverId = "I002"
val copperid = "I003"
val zincId = "I004"
val ing01= dao.insert( IngredientsEntity(ingredientId = mercuryId, Ingredient("Mercury","Quicksilver- Hazardous")))
val ing02 = dao.insert(IngredientsEntity(silverId, Ingredient("Silver","Au - Metal - Safe blah ...")))
val ing03 = dao.insert(IngredientsEntity( ingredient = Ingredient("Copper","Cu - Metal - Safe"),ingredientId = copperid))
val ing04 = dao.insert(IngredientsEntity(ingredientId = zincId,ingredient = Ingredient(ingredientDescription = "Zn - Metal - Safe", ingredientName = "Zinc")))
/* note ing?? values will be either 1 or greater (the rowid) or -1 if the row was not inserted (e.g. duplicate conflict ignored)*/
val r1Id = "R001"
val r2Id = "R002"
val r3Id = "R003"
val r4Id = "R004"
val r1 = dao.insert(ReviewsEntity(r1Id, Review("Review 1 - etc","Reviewer1")))
val r2 = dao.insert(ReviewsEntity(r2Id,Review("Review 2 - etc","Reviewer9")))
val r3 = dao.insert(ReviewsEntity(r3Id,Review("Review 3 - etc","Reviewer1")))
val r4 = dao.insert(ReviewsEntity(r4Id,Review("Review 4 - etc","Reviewer8")))
dao.insert(StoreReviewMappingEntity(store1Id,r2Id))
dao.insert(StoreReviewMappingEntity(store1Id,r3Id))
dao.insert(StoreReviewMappingEntity(store1Id,r1Id))
dao.insert(StoreReviewMappingEntity(store1Id,r4Id))
dao.insert(StoreIngredientMappingEntity(store1Id,zincId))
dao.insert(StoreIngredientMappingEntity(store1Id,mercuryId))
dao.insert(StoreIngredientMappingEntity(store1Id,copperid))
dao.insert(StoreIngredientMappingEntity(store1Id,silverId))
dao.insert(StoreIngredientMappingEntity(store1Id,zincId)) //<<<< due to onconflict ignore will not be inserted as duplicate
val sb: java.lang.StringBuilder = java.lang.StringBuilder().append("Extracting All Stores with Reviews and Ingredients:-")
for (swmrawmi: StoreWithMappedReviewsAndWithMappedIngredients in dao.getAllStoresWithReviewsAndWithIngredients()) {
sb.append("\nCurrentStore is ${swmrawmi.storeEntity.current} it has ${swmrawmi.reviewList.size} Reviews and ${swmrawmi.ingredientList.size} ingredients")
sb.append("\n\tThe Reviews Are:-")
for(r: ReviewsEntity in swmrawmi.reviewList) {
sb.append("\n\t\tID is ${r.reviewId}, Title is ${r.review.reviewTitle} etc")
}
sb.append("\n\tThe ingredients Are :-")
for(i: IngredientsEntity in swmrawmi.ingredientList) {
sb.append("\n\t\tID is ${i.ingredientId}, Name is ${i.ingredient.ingredientName} Description is ${i.ingredient.ingredientDescription}")
}
}
Log.d("RESULTINFO",sb.toString())
}
}
and the result from the log when run :-
D/RESULTINFO: Extracting All Stores with Reviews and Ingredients:-
CurrentStore is S001 it has 4 Reviews and 4 ingredients
The Reviews Are:-
ID is R001, Title is Review 1 - etc etc
ID is R002, Title is Review 2 - etc etc
ID is R003, Title is Review 3 - etc etc
ID is R004, Title is Review 4 - etc etc
The ingredients Are :-
ID is I001, Name is Mercury Description is Quicksilver- Hazardous
ID is I002, Name is Silver Description is Au - Metal - Safe blah ...
ID is I003, Name is Copper Description is Cu - Metal - Safe
ID is I004, Name is Zinc Description is Zn - Metal - Safe
CurrentStore is S002 it has 0 Reviews and 0 ingredients
The Reviews Are:-
The ingredients Are :-
I was using room but now struggling to find this type of mapping.
#Entity(tableName = "avatars")
data class AvatarEntity(
#PrimaryKey val id: Int,
#ColumnInfo(name = "user_id") var userId: Int,
var avatar: String
)
#Entity(tableName = "cities")
data class CityEntity(
#PrimaryKey val id: Int,
val name: String
)
#Entity(tableName = "driver_cities")
data class DriverCityEntity (
#PrimaryKey val id: Int,
#ColumnInfo(name = "user_id") var userId : String,
#ColumnInfo(name = "city_id") var cityId : String
)
#Entity(tableName = "drivers")
data class DriverEntity(
#PrimaryKey val id: Int,
#ColumnInfo(name = "full_name") var name: String
)
First avatars table will contain an avatar info of driver.
cities table contains a list of cities.
driver_cities table will hold a reference to driver and the its city_id.
drivers table will contain data of drivers.
Now i want to create a separate object, which will contains a driver and city of that driver, also avatar of that driver.
Just like as shown in following code.
data class DriverWithAvatarAndCity(
val driver: DriverEntity,
val avatar: AvatarEntity,
val city: CityEntity
)
if i use #Embedded and do a relation with avatar, because avatar table has user_id column, so i can get the avatar, but for city, the city_id is in separate table somehow we need the driver_city_entity as embedded here.
Well i'm totally stuck here, please guide.
Assuming that a driver has 1 avatar only and that a driver can operate in multiple cities (i.e. DriverCityEntity implies many-many) then.
For a Driver you can embed (#Embedded) the driver and get the avatar with a simple relationship (#Relation) the Cities can be obtained via the driver city mapping table using a more complex relationship (#Relation). That is in Room your associate (associateBy) the Driver with the City via the mapping table ( a Junction) e.g. :-
data class DriverWithAvatarAndCity(
#Embedded val driver: DriverEntity,
#Relation(
entity = CityEntity::class,
parentColumn = "id", // The parent is the driver
entityColumn = "id", // The child is the City(ies)
// map (associate) the driver to the city(ies) via :-
associateBy = Junction(
DriverCityEntity::class, // The mapping table
parentColumn = "user_id", // The column in the mapping table that maps the Driver
entityColumn = "city_id" // The column in the mapping table that masps the City
)
)
val city: List<CityEntity>,
#Relation(entity = AvatarEntity::class, parentColumn = "id", entityColumn = "user_id")
val avatar: AvatarEntity
)
You can then use the above in a query (#Dao) such as :-
#Query("SELECT * FROM drivers")
#Transaction
abstract fun getDriverWithCitiesAndDriverAvatar(): List<DriverWithAvatarAndCity>
Example
With AllDao as :-
#Dao
abstract class AllDao {
#Insert
abstract fun insert(avatarEntity: AvatarEntity): Long
#Insert
abstract fun insert(cityEntity: CityEntity): Long
#Insert
abstract fun insert(driverEntity: DriverEntity): Long
#Insert
abstract fun insert(driverCityEntity: DriverCityEntity): Long
#Query("SELECT * FROM drivers")
#Transaction
abstract fun getDriverWithCitiesAndDriverAvatar(): List<DriverWithAvatarAndCity>
}
using the following :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
private val TAG = "DRVINFO"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
// Adds some testing data
var londonId = dao.insert(CityEntity(1,"London"))
var parisId = dao.insert(CityEntity(2,"Paris"))
var bonnId = dao.insert(CityEntity(5,"Bonn"))
var amsertdamId = dao.insert(CityEntity(9,"Amsterdam"))
var d1 = dao.insert(DriverEntity(10,"Mary"))
var d2 = dao.insert(DriverEntity(1,"Fred"))
var a1 = dao.insert(AvatarEntity(1,d1,"Mary Avatar"))
var a2 = dao.insert(AvatarEntity(2,d2,"Fred Avatar"))
// link/map driver and cities
dao.insert(DriverCityEntity(1000,d1,londonId))
dao.insert(DriverCityEntity(1001,d1,bonnId))
dao.insert(DriverCityEntity(1002,d2,amsertdamId))
// Get all the Drivers, with the driver's avatar and the list of cities
for(dwa: DriverWithAvatarAndCity in dao.getDriverWithCitiesAndDriverAvatar()) {
Log.d(TAG,"Driver is ${dwa.driver.name} avatar is ${dwa.avatar.avatar}")
for (c: CityEntity in dwa.city) {
Log.d(TAG,"\tCity is ${c.name}")
}
}
}
}
Results in the following being included in the log:-
D/DRVINFO: Driver is Fred avatar is Fred Avatar
D/DRVINFO: City is Amsterdam
D/DRVINFO: Driver is Mary avatar is Mary Avatar
D/DRVINFO: City is London
D/DRVINFO: City is Bonn
Im noticing something weird with Room.
I have two entity: Wine and Bottles
A bottle belongs to only one wine, but a wine can have multiple bottles (one to many)
So, i've got the folowing model:
#Entity(tableName = "wine")
data class Wine(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id_wine")
val idWine: Long = 0,
val name: String
)
#Entity(tableName = "bottle")
data class Bottle(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id_bottle")
val idBottle: Long = 0,
#ColumnInfo(name = "id_wine") val idWine: Long,
val comment: String
)
The relation:
data class WineWithBottles (
#Embedded val wine: Wine,
#Relation(
parentColumn = "id_wine",
entityColumn = "id_bottle"
)
val bottles: List<Bottle>
)
And finally there is the database room prepopulate callback:
private val roomCallback: Callback = object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
thread {
val bottleDao = instance?.bottleDao()
val wineDao = instance?.wineDao()
wineDao?.insertWine(Wine(1, "a"))
wineDao?.insertWine(Wine(2, "a"))
wineDao?.insertWine(Wine(3, "a"))
bottleDao?.insertBottle(Bottle(0, 1, a))
bottleDao?.insertBottle(Bottle(0, 1, b))
bottleDao?.insertBottle(Bottle(0, 1, c))
}
}
}
I mention that i provide Wine and Bottles Entities to the Room #Database annotation
So the problem is, when i'am observing a getAllBottles() : LiveData<List<Bottle>>, i get every bottles, everything is fine
But when i'm observing getWineWithBottles(): LiveData<List<WineWithBottles>> i've got one bottle per wine, even though i've set the id_wine of all bottles at 1
Each WineWithBottles object has a wine, and a SINGLE bottle in the list:
[WineWithBottles(wine=Wine(idWine=1, name=a), bottles=[Bottle(idBottle=1, idWine=1, comment=a)]), WineWithBottles(wine=Wine(idWine=2, name=a), bottles=[Bottle(idBottle=2, idWine=1, comment=b)]), WineWithBottles(wine=Wine(idWine=3, name=a), bottles=[Bottle(idBottle=3, idWine=1, comment=c)])]
Try to change relation's condition to wine.id_wine=bottle.id_wine:
data class WineWithBottles (
#Embedded val wine: Wine,
#Relation(
parentColumn = "id_wine",
entityColumn = "id_wine"
)
val bottles: List<Bottle>
)
I'm currently working on an android application using room, with a chinese dictionary where there's a one to many relation between a character and its english definitions.
#Entity(tableName="characters")
data class Character(var mandarin : String) {
#PrimaryKey
var charcterId : Int = 0
}
#Entity(tableName = "definitions",
foreignKeys = [
ForeignKey(
entity = Character::class,
parentColumns = ["character_id"],
childColumns = ["parent_character_id"]
)
]
)
class Definition(
var english : String,
) {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "definition_id")
var definitionId : Int = 0
#ColumnInfo(name = "parent_character_id")
var parentCharacterId : Int = 0
}
I have a class that I use to store a character and its list of definitions:
data class Word (
#Embedded val character : Character,
#Relation(
parentColumn = "character_id",
entityColumn = "parent_character_id"
//,associateBy = Junction(JunctionWordXDefinition::class)
)
val definitions : MutableList<Definition>
)
My goal is to be able to allow the user to be able to create lists of these words for an activity, and so this will consist of a Many-To-Many relationship between a topic and the Words.
My question is: As android requires classes used in a junction to be either an entity, or a database view, is it possible to use my One-To-Many Word class in the juction table?
Or do I have to modify it to use a junction table?
These are the classes I've created to try and implement this, but I get an error stating Word needs to be an entity or a database view.
#Entity(tableName = "topics")
data class Topic(
val title: String = ""
) {
#ColumnInfo(name = "topic_id")
var id : Int= 0
}
#Entity(primaryKeys = ["topic_id", "character_id"])
data class JunctionTopicXWord (
val topicId : Int,
val characterId : Int
)
data class Lesson(
#Embedded var topic: Topic,
#Relation(
parentColumn = "topic_id",
entityColumn = "character_id",
associateBy = Junction(JunctionTopicXWord::class)
)
val words : List<Word>
)
There are Nested Relations in Room you can try to use without modifying your entities.
I guess it should be something like that (entity Word don't need changes):
data class Lesson(
#Embedded var topic: Topic,
#Relation(
entity = Character::class,
parentColumn = "character_id",
entityColumn = "character_id",
associateBy = Junction(JunctionTopicXWord::class)
)
val words : List<Word>
)