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 :-
Related
Parent Table :
#Entity(tableName = "Product")
data class Products (
#PrimaryKey(autoGenerate = false)
#ColumnInfo(name = "id")
var id : Int = 0,
#ColumnInfo(name = "name")
var name : String? = null,
#ColumnInfo(name = "variants")
var variants : MutableList<Variants> = mutableListOf()
)
Child Table :
#Entity(tableName = "Variant")
data class Variants (
#PrimaryKey(autoGenerate = false)
#ColumnInfo(name = "id")
var id : Int = 0,
#ColumnInfo(name = "product_id", index = true)
var product_id : Int? = null,
#ColumnInfo(name = "measurement")
var measurement : String? = null,
#ColumnInfo(name = "discounted_price")
var discounted_price : String? = null,
#ColumnInfo(name = "cart_count")
var cart_count : Int? = null
)
i want to update the cart_count in variant and it should also reflect in product table also as variant updated .. what is the query for this ??
When i use this Update query ..i update the value in Variant Table but when i get getallProducts , the variant table shows old value instead of new updated value
My Update Query :
#Query("UPDATE Variant SET cart_count= :cart_count, is_notify_me= :is_Notify,product_id= :product_id WHERE id = :id")
fun updateVariant(id: Int,is_Notify:Boolean, cart_count: String,product_id: Int) : Int
It doesn't work when i getProducts using this Query :
#Transaction
#Query("SELECT * FROM Product WHERE subcategory_id=:subcatid")
fun getAllProducts(subcatid:Int): Flow<MutableList<Products>>
Actually the Get Query is Correct but Update query is wrong
Create data class like below
data class ProductWithVariants(
#Embedded val product: Product,
#Relation(
parentColumn = "id",
entityColumn = "productId"
)
var variamts: List<Variant>? = null,
)
And in the Dao
#Transaction
#Query("SELECT * FROM product WHERE id:id")
suspend fun getProduct(id: Int): List<ProductWithVariants>
That's all, After the update variant select query will fetch the data from both tables and combine it.
You can add a foreign key for referral integrity.
I believe that you misunderstand the relationships.
That is, you have a list (MutableList) stored in the Product table, but are updating the row (if one) of the Variant in the variant table, you are then appearing to extract the Product and thus the reconstructed Variant stored in the Product table, not the updated Variant that is a child (again if it exists) in the Variant table.
There is probably no reason to have the Products include :-
#ColumnInfo(name = "variants")
var variants : MutableList<Variants> = mutableListOf()
And my guess is this is what is throwing you.
Example/Demo
Perhaps consider this demonstration of what may be confusing you. The demo is based upon your code although there are some alterations (typically commented). The demo is purposefully wrong as it includes both data being store as part of the product and also the exact same core data being store in the Variant table (the latter being the better for your situation of say updating the cart_count).
The Products data class:-
#Entity(tableName = "Product")
data class Products (
#PrimaryKey(autoGenerate = false)
#ColumnInfo(name = "id")
var id : Int? = null, /* allows generation of the id, if need be (should really be Long though )*/
#ColumnInfo(name = "name")
var name : String? = null,
#ColumnInfo(name = "variants")
//var variants : MutableList<Variants> = mutableListOf()
/* Note changed to suit com.google.code.Gson */
/* Note probably not a required column anyway */
var variantsMutableListHolder: VariantsMutableListHolder
)
The main change is just to suit what little I know of JSON. However as the suggestion is that variantsMutableListHolder is not required, ignore this.
VariantsMutableListHolder (not required for the suggested solution):-
class VariantsMutableListHolder(
val variantsMutableList: MutableList<Variants>
)
Converters (for the VariantsMutableListHolder, again not required for the suggested solution) :-
class Converters {
#TypeConverter
fun fromVariantsMutableListToJSONString(variantsMutableListHolder: VariantsMutableListHolder): String = Gson().toJson(variantsMutableListHolder)
#TypeConverter
fun fromJSONStringToVariantsMutableListHolder(jsonString: String): VariantsMutableListHolder=Gson().fromJson(jsonString,VariantsMutableListHolder::class.java)
}
Variants (mainly suggested changes for referential integrity (protect against orphans)) :-
#Entity(
tableName = "Variant",
/* You may wish to consider adding Foreign Key constraints */
/* FK constraints enforce referential integrity*/
foreignKeys = [
ForeignKey(
entity = Products::class, /* The Parent Class */
parentColumns = ["id"], /* The column or columns (if composite key) that map to the parent */
childColumns = ["product_id"], /* the column or columns in the child that reference the parent */
/* Optional but assists in maintaining referential integrity automatically */
onDelete = ForeignKey.CASCADE, /* if a parent is deleted then so are the children */
onUpdate = ForeignKey.CASCADE /* if the reference column in the parent is changed then the value in the children is changed */
)
]
)
data class Variants (
#PrimaryKey(autoGenerate = false)
#ColumnInfo(name = "id")
var id : Int? = null, /* allows generation of id, if null passed */
#ColumnInfo(name = "product_id", index = true)
var product_id : Int? = null,
#ColumnInfo(name = "measurement")
var measurement : String? = null,
#ColumnInfo(name = "discounted_price")
var discounted_price : String? = null,
#ColumnInfo(name = "cart_count")
var cart_count : Int? = null
)
ProductsWithRelatedVariants NEW IMPORTANT class:-
/* Class for retrieving the Product with the children from the Variant table */
data class ProductsWithRelatedVariants(
#Embedded
var products: Products,
#Relation(
entity = Variants::class, /* The class of the Children */
parentColumn = "id", /* the column in the parent table that is referenced */
entityColumn = "product_id" /* The column in the child that references the parent*/
)
var variantsList: List<Variants>
)
AllDao all the dao functions (note no Flows/Suspends as mainthread used for the demo) :-
#Dao
interface AllDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(products: Products): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(variants: Variants): Long
/* adjusted to suit code in question */
#Query("UPDATE Variant SET cart_count= :cart_count /*, is_notify_me= :is_Notify*/ ,product_id= :product_id WHERE id = :id")
fun updateVariant(id: Int/*,is_Notify:Boolean*/, cart_count: String,product_id: Int) : Int
#Query("SELECT * FROM Variant WHERE id=:id")
fun getVariantsById(id: Int): Variants
/* adjusted to suit code in question */
#Transaction
#Query("SELECT * FROM Product /*WHERE subcategory_id=:subcatid*/")
fun getAllProducts(/*subcatid:Int*/): /*Flow<*/MutableList<Products>/*>*/ /*As run on main thread no flow needed */
#Transaction
#Query("SELECT * FROM Product")
fun getAllProductsWithTheRelatedVariants(): MutableList<ProductsWithRelatedVariants>
}
TheDatabase the #Database annotated class so demo can be run :-
#TypeConverters(Converters::class)
#Database(entities = [Products::class,Variants::class], exportSchema = false, version = 1)
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() /* for convenience brevity run on the main thread */
.build()
}
return instance as TheDatabase
}
}
}
MainActivity putting the above into action:-
const val TAG = "DBINFO"
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 productId=100
var vm1 = mutableListOf<Variants>(
Variants(measurement = "10 inches", discounted_price = "11.99", cart_count = 10, product_id = 100),
Variants(measurement = "10 ounces", discounted_price = "2.50", cart_count = 5, product_id = 100),
Variants(measurement = "100 grams", discounted_price = "1.75", cart_count = 3, product_id = 100)
)
dao.insert(Products(100, name = "Product1",VariantsMutableListHolder(vm1)))
/* Insert the related variants */
val insertedVariantIdList: ArrayList<Long> = ArrayList(0)
for (v in vm1) {
insertedVariantIdList.add(dao.insert(v))
}
/* Update the 2nd Variants */
dao.updateVariant(insertedVariantIdList[1].toInt(),"99",productId)
/* Used for building output data (both)*/
val sb = StringBuilder()
/*STG001*/
/* Extract data just stored in the Product table */
for(p in dao.getAllProducts()) {
sb.clear()
for (v in p.variantsMutableListHolder.variantsMutableList) {
sb.append("\n\tMSR=${v.measurement} DP=${v.discounted_price} CC=${v.cart_count}")
}
Log.d(TAG+"+STG001","PRODUCT NAME IS ${p.name} it has ${p.variantsMutableListHolder.variantsMutableList.size} variants; they are:-$sb")
}
/*STG002*/
/* Extract the data from the Product Table along with the related variants i.e. ProductsWithRelatedVariants */
for(pwrv in dao.getAllProductsWithTheRelatedVariants()) {
sb.clear()
for (v in pwrv.variantsList) {
sb.append("\n\tMSR=${v.measurement} DP=${v.discounted_price} CC=${v.cart_count}")
}
Log.d(TAG+"+STG002","PRODUCT NAME IS ${pwrv.products.name} it has ${pwrv.products.variantsMutableListHolder.variantsMutableList.size} variants; they are:-$sb")
}
}
}
So when run (note only meant to be run once to demo):-
Sets the Product id to be used to 100
just one product needed to demo
100 could be any value, just 100 is easy and demonstrates setting a specific id.
Builds a MutableList as the Variants data to both be stored as part of the Product (the suggested bit to skip) and as rows in the Variant table.
note id's are irrelevant here if as suggested storing Variants in the variants table
Inserts the Product along with the Variants in the Product table.
Loops through the MutableList inserting each Variants into the Variant table, the id being generated and stored in the ArrayList. The product_id being set to the Product row that was inserted.
Ideally each insert should be checked, as if the returned id is -1 then the row has not been inserted due to a conflict (beyond the scope of this question though).
NOTE if a ForeignKey conflict (e.g. 99 given as the product_id not 100) then App would fail (beyond the scope of the question to go into handling that situation).
UPDATE one of the cart_counts (2nd variant changed to 99)
retrieve all the products and output the product details and the Variants stored in the Product table details (i.e. cart_count NOT changed)
retrieve all the products with the related variants from the variant table as a list of ProductsWithRelatedVariants (could be a Flow).
Result (as output to the Log) :-
D/DBINFO+STG001: PRODUCT NAME IS Product1 it has 3 variants; they are:-
MSR=10 inches DP=11.99 CC=10
MSR=10 ounces DP=2.50 CC=5
MSR=100 grams DP=1.75 CC=3
D/DBINFO+STG002: PRODUCT NAME IS Product1 it has 3 variants; they are:-
MSR=10 inches DP=11.99 CC=10
MSR=10 ounces DP=2.50 CC=99
MSR=100 grams DP=1.75 CC=3
As can be seen, the near exact same data is extracted BUT CC=5 is still stored in the Product table, whilst the in the variants second and suggested way, CC=99.
Furthermore, have a look at the Data in the database:-
noting the highlighted bloated (unnecessary data) as opposed to the same data in the Variant table:-
An Alternative
The alternative would be to Update the Product Table, here's a risky example of how:-
UPDATE Product SET variants = substr(variants,1,instr(variants,'"cart_count":5,"'))||'"cart_count":99,"'||substr(variants,instr(variants,'"cart_count":5,"') + length('"cart_count":5,"')) WHERE id=100 AND instr(variants,'"cart_count":5,"');
Note App Inspection took an exception to SQLite's replace function, so instead the substr and instr functions were used.
NOTE this query has not been tested to cope with all scenarios and may well result in unpredictable results, it would not cope with the column name being changed as an example.
After running the query then the Product table has :-
Oooops looks like I got too many double quotes somewhere (good example of how flimsy this approach would be).
Suggested Solution
Here's the suggested solution with the code trimmed down to just what is needed.
#Entity(tableName = "Product")
data class Products (
#PrimaryKey
var id : Int? = null, /* allows generation of the id, if need be (should really be Long though )*/
var name : String? = null,
)
#Entity(
tableName = "Variant",
/* You may wish to consider adding Foreign Key constraints */
/* FK constraints enforce referential integrity*/
foreignKeys = [
ForeignKey(
entity = Products::class, /* The Parent Class */
parentColumns = ["id"], /* The column or columns (if composite key) that map to the parent */
childColumns = ["product_id"], /* the column or columns in the child that reference the parent */
/* Optional but assists in maintaining referential integrity automatically */
onDelete = ForeignKey.CASCADE, /* if a parent is deleted then so are the children */
onUpdate = ForeignKey.CASCADE /* if the reference column in the parent is changed then the value in the children is changed */
)
]
)
data class Variants (
#PrimaryKey
var id : Int? = null, /* allows generation of id, if null passed */
#ColumnInfo(index = true)
var product_id : Int? = null,
var measurement : String? = null,
var discounted_price : String? = null,
var cart_count : Int? = null
)
/* Class for retrieving the Product with the children from the Variant table */
data class ProductsWithRelatedVariants(
#Embedded
var products: Products,
#Relation(
entity = Variants::class, /* The class of the Children */
parentColumn = "id", /* the column in the parent table that is referenced */
entityColumn = "product_id" /* The column in the child that references the parent*/
)
var variantsList: List<Variants>
)
#Dao
interface AllDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(products: Products): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(variants: Variants): Long
/* adjusted to suit code in question */
#Query("UPDATE Variant SET cart_count= :cart_count WHERE id = :id")
fun updateVariant(id: Int, cart_count: String) : Int
#Query("SELECT * FROM Variant WHERE id=:id")
fun getVariantsById(id: Int): Variants
/* adjusted to suit code in question */
#Transaction
#Query("SELECT * FROM Product /*WHERE subcategory_id=:subcatid*/")
fun getAllProducts(): /*Flow<*/MutableList<Products>/*>*/ /*As run on main thread no flow needed */
#Transaction
#Query("SELECT * FROM Product")
fun getAllProductsWithTheRelatedVariants(): MutableList<ProductsWithRelatedVariants>
}
#Database(entities = [Products::class,Variants::class], exportSchema = false, version = 1)
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() /* for convenience brevity run on the main thread */
.build()
}
return instance as TheDatabase
}
}
}
And to test:-
const val TAG = "DBINFO"
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 productId=100
var vm1 = mutableListOf<Variants>(
Variants(measurement = "10 inches", discounted_price = "11.99", cart_count = 10, product_id = 100),
Variants(measurement = "10 ounces", discounted_price = "2.50", cart_count = 5, product_id = 100),
Variants(measurement = "100 grams", discounted_price = "1.75", cart_count = 3, product_id = 100)
)
dao.insert(Products(100, name = "Product1"))
/* Insert the related variants */
val insertedVariantIdList: ArrayList<Long> = ArrayList(0)
for (v in vm1) {
v.product_id = productId
insertedVariantIdList.add(dao.insert(v))
}
/* Update the 2nd Variants */
dao.updateVariant(insertedVariantIdList[1].toInt(),"99")
/* Used for building output data (both)*/
val sb = StringBuilder()
/*STG002*/
/* Extract the data from the Product Table along with the related variants i.e. ProductsWithRelatedVariants */
for(pwrv in dao.getAllProductsWithTheRelatedVariants()) {
sb.clear()
for (v in pwrv.variantsList) {
sb.append("\n\tMSR=${v.measurement} DP=${v.discounted_price} CC=${v.cart_count}")
}
Log.d(TAG+"+STG002","PRODUCT NAME IS ${pwrv.products.name} it has ${pwrv.variantsList.size} variants; they are:-$sb")
}
}
}
And the Result in the log (using the trimmed code):-
D/DBINFO+STG002: PRODUCT NAME IS Product1 it has 3 variants; they are:-
MSR=10 inches DP=11.99 CC=10
MSR=10 ounces DP=2.50 CC=99
MSR=100 grams DP=1.75 CC=3
Let's say I have an Article table like this:
Article {
id;
title;
content;
main_reference_id;
secondary_reference_id;
}
And there is a Reference table somewhat like this:
Reference {
id;
other_reference_related_columns...;
}
Now I want to fetch the Article while also fetching main reference, secondary reference with another POJO like this:
data class ArticleFull(
#Embedded
val article: article,
#Relation(parentColumn = "main_reference_id", entityColumn = "id")
val main_reference: Reference,
#Relation(parentColumn = "secondary_reference_id", entityColumn = "id")
val other_reference: Reference
)
But I'm not sure what I wrote is the right usage of #Relation annotation or not.
N.B.: I'm from the Laravel/Eloquent background, So I'm more familiar with these belongsTo, hasOne, hasMany, belongsToMany, and so on relationship types.
Thanks.
But I'm not sure what I wrote is the right usage of #Relation annotation or not.
Yes that is fine.
Here's a working example. That shows the use of the ArticleFull POJO:-
First the Entities (Tables):-
Reference :-
#Entity
data class Reference(
#PrimaryKey
val id: Long? = null,
val other_data: String
)
Article :-
#Entity(
foreignKeys = [
ForeignKey(
entity = Reference::class,
parentColumns = ["id"],
childColumns = ["main_reference_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = Reference::class,
parentColumns = ["id"],
childColumns = ["secondary_reference_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Article(
#PrimaryKey
val id: Long? = null,
val title: String,
val content: String,
#ColumnInfo(index = true)
val main_reference_id: Long,
#ColumnInfo(index = true)
val secondary_reference_id: Long
)
Foreign Key constraints added, they help to enforce referential integrity, they are optional. The onDelete and onUpdate are optional within the Foreign Key.
An #Dao class (abstract class rather than interface, abstract class is more versatile) ArticleAndReferenceDao :-
#Dao
abstract class ArticleAndReferenceDao {
#Insert
abstract fun insert(reference: Reference): Long
#Insert
abstract fun insert(article: Article): Long
#Transaction
#Query("SELECT * FROM article")
abstract fun getAllArticleFull(): List<ArticleFull>
#Transaction#Query("SELECT * FROM article WHERE id=:articleId")
abstract fun getArticleFullByArticleId(articleId: Long): List<ArticleFull>
}
An #Database class ArticleDatabase :-
#Database(entities = [Reference::class,Article::class],version = 1)
abstract class ArticleDatabase: RoomDatabase() {
abstract fun getArticleAndReferenceDao(): ArticleAndReferenceDao
companion object {
#Volatile
private var instance: ArticleDatabase? = null
fun getArticleDatabaseInstance(context: Context): ArticleDatabase {
if(instance == null) {
instance = Room.databaseBuilder(
context,
ArticleDatabase::class.java,
"article.db"
)
.allowMainThreadQueries()
.build()
}
return instance as ArticleDatabase
}
}
}
Finally some Activity code , noting that for convenience and brevity .allowMainThreadQueries has been used allow the code to be run on the main thread :-
class MainActivity : AppCompatActivity() {
lateinit var articleDatabase: ArticleDatabase
lateinit var articleAndReferenceDao: ArticleAndReferenceDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
articleDatabase = ArticleDatabase.getArticleDatabaseInstance(this)
articleAndReferenceDao = articleDatabase.getArticleAndReferenceDao()
val ref1 = articleAndReferenceDao.insert(Reference(other_data = "Reference1"))
val ref2 = articleAndReferenceDao.insert(Reference(other_data = "Reference2"))
val ref3 = articleAndReferenceDao.insert(Reference(other_data = "Reference3"))
val ref4 = articleAndReferenceDao.insert(Reference(other_data = "Reference4"))
articleAndReferenceDao.insert(Article(title = "Article1",main_reference_id = ref1,secondary_reference_id = ref2, content = "Content for Article1"))
articleAndReferenceDao.insert(Article(title = "Article2", main_reference_id = ref3, secondary_reference_id = ref4,content = "Content for Article2"))
// AND/OR
articleAndReferenceDao.insert(
Article(
title = "Article3",
content = "Content for Article 3",
main_reference_id = articleAndReferenceDao.insert(Reference(other_data = "Reference5")),
secondary_reference_id = articleAndReferenceDao.insert(Reference(other_data = "reference6"))
)
)
for(d: ArticleFull in articleAndReferenceDao.getAllArticleFull()) {
Log.d("ARTICLEINFO"," Article is ${d.article.content} ID is ${d.article.id} " +
"\n\tMain Reference is ${d.main_reference.other_data} ID is ${d.main_reference.id}" +
"\n\tSecondary Reference is ${d.other_reference.other_data} ID is ${d.other_reference.id}")
}
}
}
Running the above results in the log containing :-
D/ARTICLEINFO: Article is Content for Article1 ID is 1
Main Reference is Reference1 ID is 1
Secondary Reference is Reference2 ID is 2
D/ARTICLEINFO: Article is Content for Article2 ID is 2
Main Reference is Reference3 ID is 3
Secondary Reference is Reference4 ID is 4
D/ARTICLEINFO: Article is Content for Article 3 ID is 3
Main Reference is Reference5 ID is 5
Secondary Reference is reference6 ID is 6
Additional
You can also use #Embedded for all three parts.
The advantages are :-
more flexible filtering i.e. you can filter on the children (with #Relationship although you can, you need to defines the JOIN's)
Instead of multiple underlying queries for an #Relationship a single query retrieves all data
The disadvantages are:-
more complex query
requirement to use #ColumnInfo's prefix = annotation if column names are not unique to disambiguate them and thus more complex query to name the output columns accordingly.
So you could have :-
data class ArticleFullAlternative(
#Embedded
val article: Article,
#Embedded(prefix = "main_")
val main_reference: Reference,
#Embedded(prefix = "other_")
val other_reference: Reference
)
Along with an #Query such as :-
#Query("SELECT article.*, " +
/* as prefix = "main_" has been used then rename output columns accordingly */
"m.id AS main_id, m.other_data AS main_other_data, " +
/* as prefix = "other_" has been used then rename output columns accordingly */
"o.id AS other_id, o.other_data AS other_other_data " +
"FROM article " +
"JOIN reference AS m /*<<<<< to disambiguate column names */ ON main_reference_id = m.id " +
"JOIN reference AS o /*<<<<< to disambiguate column names */ ON main_reference_id = o.id ")
abstract fun getArticleFullAlternative(): List<ArticleFullAlternative>
An example use in an Activity could be :-
for(afa: ArticleFullAlternative in articleAndReferenceDao.getArticleFullAlternative()) {
Log.d("ALTARTICLEINFO"," Article is ${afa.article.content} ID is ${afa.article.id} " +
"\n\tMain Reference is ${afa.main_reference.other_data} ID is ${afa.main_reference.id}" +
"\n\tSecondary Reference is ${afa.other_reference.other_data} ID is ${afa.other_reference.id}")
}
This produces exactly the same output
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
I have implemented a DB where I have two one-to-many relationships but it would seem that room does not allow it. Is that so?
The entities are:
#Entity(tableName = "arete_sheet")
data class EAreteSheet(
#PrimaryKey val id: Long,
#ColumnInfo(name = "sheet") val form: Sheets,
#ColumnInfo(name = "version") val version: Int,
)
#Entity(tableName = "arete_sheet_paragraph")
data class EAreteSheetParagraph(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_id") val sheet: Long,
#ColumnInfo(name = "name") val name: String
)
#Entity(tableName = "arete_sheet_form")
data class EAreteSheetForm(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_paragraph_id") val paragraph: Long,
#ColumnInfo(name = "fieldType") val fieldType: FieldType,
#ColumnInfo(name = "cell") val cell: String,
#ColumnInfo(name = "label") val label: String
)
To solve the schema I have implemented these join classes:
data class EAreteSheetWithParagraph(
#Embedded val sheet: EAreteSheet,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_id"
)
val paragraph: List<EAreteSheetParagraphWithForm>
)
data class EAreteSheetParagraphWithForm(
#Embedded val paragraph: EAreteSheetParagraph,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_paragraph_id"
)
val forms: List<EAreteSheetForm>
)
This is the DAO implementation:
#Transaction
#Query("SELECT * FROM arete_sheet")
suspend fun getSheetWithParagraphsAndForms(): List<EAreteSheetWithParagraph>
This is the mistake he gives me in the building phase:
app/build/generated/source/kapt/debug/it/ximplia/agri2000/model/db/dao/AreteSheetDAO_Impl.java:203: error: constructor EAreteSheetWithParagraph in class EAreteSheetWithParagraph cannot be applied to given types;
_item = new EAreteSheetWithParagraph();
^
required: EAreteSheet,List<EAreteSheetParagraphWithForm>
found: no arguments
reason: actual and formal argument lists differ in length
app/build/generated/source/kapt/debug/it/ximplia/agri2000/model/db/dao/AreteSheetDAO_Impl.java:204: error: sheet has private access in EAreteSheetWithParagraph
_item.sheet = _tmpSheet;
I think that Room does not allow to resolve dependencies in cascade but I would like to know if someone was successful or if I made a mistake before changing the code.
In EAreteSheetWithParagraph you are specifying a list of EAreteSheetParagraphWithForm's
as per :-
val paragraph: List<EAreteSheetParagraphWithForm>
The #Relation will try to ascertain the columns as per the EAreteSheetParagraphWithForm form class, which is not an Entity. You should change the #Relation to specify the appropriate entity (i.e. the EAreteSheetParagraph) using the entity parameter.
So EAreteSheetWithParagraph should be something like :-
data class EAreteSheetWithParagraph(
#Embedded val sheet: EAreteSheet,
#Relation(
entity = EAreteSheetParagraph::class,
parentColumn = "id",
entityColumn = "arete_sheet_id"
)
val paragraph: List<EAreteSheetParagraphWithForm>
)
However, I don't believe that you are getting that far, as the messages appear to be complaining about the sheet variable which has a type of Sheets (and that the variable is private).
Without adding the entity= parameter then the compile, if it reached that stage, would fail with :-
> Task :app:kaptDebugKotlin FAILED
error: The class must be either #Entity or #DatabaseView. - a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm
E:\AndroidStudioApps\SO68953488KotlinRoomMany1N\app\build\tmp\kapt3\stubs\debug\a\a\so68953488kotlinroommany1_n\EAreteSheetWithParagraph.java:12: error: Cannot find the child entity column `arete_sheet_id` in a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm. Options:
private final java.util.List<a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm> paragraph = null;
^
The class must be either #Entity or #DatabaseView. - a.a.so68953488kotlinroommany1_n.EAreteSheetParagraphWithForm
I think that Room does not allow to resolve dependencies in cascade but I would like to know if someone was successful or if I made a mistake before changing the code.
As indicated above, I believe the issue is mistakes so :-
Proof of concept
Based upon your code the following shows that nested/multiple 1-n's do work:-
Using your code BUT with the following changes (to avoid issues and simplify) :-
#Entity(tableName = "arete_sheet_form")
data class EAreteSheetForm(
#PrimaryKey val id: Long,
#ColumnInfo(name = "arete_sheet_paragraph_id") val paragraph: Long,
#ColumnInfo(name = "fieldType") val fieldType: /* FieldType */ String, // changed for convenience/brevity
#ColumnInfo(name = "cell") val cell: String,
#ColumnInfo(name = "label") val label: String
)
fieldType' stype changed from FieldType to String so no need for extra class and type converters.
and
#Entity(tableName = "arete_sheet")
data class EAreteSheet(
#PrimaryKey val id: Long,
#ColumnInfo(name = "sheet") val form: /*Sheets*/ String, // changed for convenience/brevity
#ColumnInfo(name = "version") val version: Int,
)
Sheets type substituted with String
EAreteSheetWithParagraph as above
EAreteSheetParagraphWithForm changed to
data class EAreteSheetParagraphWithForm(
#Embedded val paragraph: EAreteSheetParagraph,
#Relation(
parentColumn = "id",
entityColumn = "arete_sheet_paragraph_id",
entity = EAreteSheetForm::class
)
val forms: List<EAreteSheetForm>
)
i.e. the entity parameter has been added according to my preference to always code the entity parameter.
AreteSheetDao used :-
#Dao
abstract class AreteSheetDAO {
#Insert
abstract fun insert(eAreteSheet: EAreteSheet): Long
#Insert
abstract fun insert(eAreteSheetParagraph: EAreteSheetParagraph): Long
#Insert
abstract fun insert(eAreteSheetForm: EAreteSheetForm): Long
#Transaction
#Query("SELECT * FROM arete_sheet")
abstract fun getSheetWithParagraphsAndForms(): List<EAreteSheetWithParagraph>
}
Code in an Activity :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AreteSheetDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getDao()
var s1 = dao.insert(EAreteSheet(10,"Sheet1",1))
var s2 = dao.insert(EAreteSheet(20,"Sheet2",1))
var p1 = dao.insert(EAreteSheetParagraph(100,s1,"Para1 (Sheet1)"))
var p2 = dao.insert(EAreteSheetParagraph(101,s1,"Para2 (Sheet1)"))
var p3 = dao.insert(EAreteSheetParagraph(201,s2,"Para3 (Sheet2)"))
var p4 = dao.insert(EAreteSheetParagraph(202,s2,"Para4 (Sheet2)"))
var f1 = dao.insert(EAreteSheetForm(1000,p1,"typex","cellx","Form1"))
var f2 = dao.insert(EAreteSheetForm(1001,p1,"typex","cellx","Form2"))
var f3 = dao.insert(EAreteSheetForm(1002,p1,"typex","cellx","Form3"))
var f4 = dao.insert(EAreteSheetForm(1010,p2,"typex","cellx","Form4"))
var f5 = dao.insert(EAreteSheetForm(1011,p2,"typex","cellx","Form5"))
var f6 = dao.insert(EAreteSheetForm(1020,p3,"typex","cellx","Form6"))
val TAG = "ARETEINFO"
for(sw: EAreteSheetWithParagraph in dao.getSheetWithParagraphsAndForms()) {
Log.d(TAG,"Sheet ID is ${sw.sheet.id} Form is ${sw.sheet.form} Version is ${sw.sheet.version}" )
for(pf: EAreteSheetParagraphWithForm in sw.paragraph) {
Log.d(TAG,"\tPara is ${pf.paragraph.name} etc")
for(f: EAreteSheetForm in pf.forms) {
Log.d(TAG,"\t\tForm is ${f.label}")
}
}
}
}
}
As can be seen, some data is loaded, 2 Sheets, each with 2 paragraphs and then 4 forms distributed unevenly (3 to para1, 2 to para2, 1 to para3, 0 to para4).
The data is then extracted (as array of EAreteSheetWithParagraph), the array traversed (traversing the underlying arrays of paragraphs and forms within paragraphs) and output to the log the Result being :-
D/ARETEINFO: Sheet ID is 10 Form is Sheet1 Version is 1
D/ARETEINFO: Para is Para1 (Sheet1) etc
D/ARETEINFO: Form is Form1
D/ARETEINFO: Form is Form2
D/ARETEINFO: Form is Form3
D/ARETEINFO: Para is Para2 (Sheet1) etc
D/ARETEINFO: Form is Form4
D/ARETEINFO: Form is Form5
D/ARETEINFO: Sheet ID is 20 Form is Sheet2 Version is 1
D/ARETEINFO: Para is Para3 (Sheet2) etc
D/ARETEINFO: Form is Form6
D/ARETEINFO: Para is Para4 (Sheet2) etc
I have two related entities:
Station
#Entity(tableName = "stations")
data class Station(
#PrimaryKey
#ColumnInfo(name = "id")
val id: Long,
#ColumnInfo(name = "latitude")
val latitude: Double,
#ColumnInfo(name = "longitude")
val longitude: Double,
#ColumnInfo(name = "connectors")
val connectors: List<Connector>, // this field has a type converter
)
Connector
#Entity(
tableName = "connectors",
primaryKeys = ["station_id", "station_connector_id"]
)
class Connector(
#ColumnInfo(name = "station_connector_id")
val stationConnectorId: Int,
#ColumnInfo(name = "station_id")
val stationId: Long,
#ColumnInfo(name = "type")
val type: ConnectorType,
)
When I insert data, I fill up both entites and it seems kinda okay, but when I'm trying to receive stations with particular types of connectors it duplicates rows.
For example, I have an object
Station(
id = 100,
latitude = 56.565,
longitude = 34.565,
connectors = [
Connector(stationConnectorId=1, stationId=100, type=TYPE_2),
Connector(stationConnectorId=2, stationId=100, type=CHADEMO),
Connector(stationConnectorId=3, stationId=100, type=TYPE_1)
]
)
And if I want to filter stations only by one connector type I receive one row with this station(and it's right), but if I want to reset filters and look up for stations that can contain many connectors, I receive duplicates of this stations(in this example if I request for station with TYPE_1, TYPE_2 and CHADEMO connector types it will be three equal rows).
I'm using this query to request stations from my database:
SELECT * FROM simple_stations
INNER JOIN connectors ON simple_stations.id = connectors.station_id
WHERE connectors.type IN (:connectorTypesList)
I've tried to use DISTINCT in the query, ForeignKeys and Indexes in these Entities, but it was not working, so now I'm completely lost.
If you just want Stations then you have various options then your have various options.
is to use a GROUP BY clause such as GROUP BY simple_stations.id
However, the issue you may then encounter is that Station would be incomplete/unreliable as you have a List of Connectors and if you GROUP by Station then you will only get a single arbitrary Connector (there again that may depend upon you TypeConvertor).
to use DISTINCT you would have to only include the Station columns (similar problem as above).
I'd suggest that your schema is at fault by including the List of Connectors related to the Station you are duplicating data (aka it's not normalised).
rather if you removed
#ColumnInfo(name = "connectors")
val connectors: List<Connector>, // this field has a type converter
from the Station Entity the data itself would still be available for retrieval.
You may then wish to have a POJO that Embeds the Station and has a List of Connector's, perhaps one with and one without the Connector List have an #Relationship (with and you would get all connectors irrespective of the WHERE clause as that's how #Relationship works). Without and you could have the constructor get only the Connectors with the types you want.
Perhaps consider the following based upon your code:-
The Station Entity
#Entity(tableName = "stations")
data class Station(
#PrimaryKey
#ColumnInfo(name = "id")
val id: Long,
#ColumnInfo(name = "latitude")
val latitude: Double,
#ColumnInfo(name = "longitude")
val longitude: Double
/*
#ColumnInfo(name = "connectors")
val connectors: List<Connector> // this field has a type converter
NO NEED IMPLIED BY RELATIONSHIP
*/
)
The Connector Entity
#Entity(
tableName = "connectors",
primaryKeys = ["station_id", "station_connector_id"]
)
class Connector(
#ColumnInfo(name = "station_connector_id")
val stationConnectorId: Int,
#ColumnInfo(name = "station_id")
val stationId: Long,
#ColumnInfo(name = "type")
val type: String //<<<<< changed for convenience
)
The StationWithConnectors POJO NEW
class StationWithConnectors {
#Embedded
var station: Station? = null
var connectors: List<Connector> = emptyList()
constructor(allDao: AllDao, station: Station, connectorTypeList: List<String>) {
this.station = station
this.connectors = allDao.getConnectorsOfSpecifiedTypesByStationId(station.id,connectorTypeList)
}
}
note the embedded query to build the list of connectors of only the specfified types
The Dao used i.e. AllDao
#Dao
interface AllDao {
#Insert
fun insert(station: Station): Long
#Insert
fun insert(connector: Connector): Long
#Query("SELECT * FROM stations")
fun getAllStations(): List<Station>
#Query("SELECT * FROM stations WHERE stations.id = :stationId")
fun getStationById(stationId: Long): Station
// Gets the Connectors per Station of the requested Type (i.e. NOT ALL CONNECTORS necessarily)
#Query("SELECT * FROM connectors WHERE station_id = :stationId AND connectors.type IN( :connectorTypesList)")
fun getConnectorsOfSpecifiedTypesByStationId(stationId: Long, connectorTypesList: List<String>): List<Connector>
// Gets the Stations that have Connectors of the requested type DISTINCT used along with only the station columns
#Query("SELECT DISTINCT stations.id, stations.latitude, stations.longitude " +
"FROM stations INNER JOIN connectors ON connectors.station_id = stations.id " +
"WHERE connectors.type IN(:connectorTypesList)")
fun getStationsWithSpecificConnectorTypes(connectorTypesList: List<String>): List<Station>
}
The #Database TheDatabase
#Database(entities = [Connector::class,Station::class],version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
}
and finaly an Activity to test/demonstrate
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 = Room.databaseBuilder(this,TheDatabase::class.java,"thedb.db")
.allowMainThreadQueries()
.build()
dao = db.getAllDao()
// Define some types
var type1 = "TYPE_1"
var type2 = "TYEP_2"
var type3 = "CHADEMO"
var type4 = "ANOTHER"
// Define stations with Connectors
var station1 = Station(100,56.565,34.565)
dao.insert(station1)
dao.insert(Connector(10,station1.id,type1))
dao.insert(Connector(20,station1.id,type4))
dao.insert(Connector(30,station1.id,type3))
dao.insert(Connector(40,station1.id ,type2))
var station2 = Station(200,33.333,22.222)
dao.insert(station2)
dao.insert(Connector(100,station2.id,type2))
dao.insert(Connector(110,station2.id,type4))
dao.insert(Connector(120,station2.id,type3))
// Define the search types
var listOfTypes = listOf(type1,type2) // Types to search for
// prepare the StationWithConnectors list
var allswcList: ArrayList<StationWithConnectors> = ArrayList()
// Get the stations with connectors of the required types
var stationsWithCertainTYpes = dao.getStationsWithSpecificConnectorTypes(listOfTypes)
// Build the StationWithCertainTypes POJOs
for(s: Station in stationsWithCertainTYpes) {
allswcList!!.add(StationWithConnectors(dao,s, listOfTypes))
}
var count = stationsWithCertainTYpes.size //<<<< just so breakpoint can be added
}
}
When run in debug mode then:-
StationWithCertainTypes gets both Stations (station 1 has type1 and type2, station2 has type2) as per :-
allswcList has the 2 StationWithConnectors built from the 2 Stations as per :-