How to map multiple embedded tables in room database? - android

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

Related

Update, insert and get element from RoomDatabase colum

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 :-

Null Data Returned with Nested Relation in Room Database Android

Given that I have 3 entities, Order contains list of LineItem, each LineItem will associates with one Product by productId.
The problem that when I get data from OrderDao, it returns null for the product field, but in the lineItem field, it has data. While I can data with ProductWithLineItem.
Already tried a lot of work arounds but it does not work.
Here is my code for entities and dao
Entities
#Entity(tableName = DataConstant.ORDER_TABLE)
data class Order(
#PrimaryKey
#ColumnInfo(name = "orderId")
val id: String,
#ColumnInfo(name = "status")
var status: String
)
#Entity(tableName = DataConstant.LINE_ITEM_TABLE)
data class LineItem(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "lineItemId")
val id: Long,
#ColumnInfo(name = "productId")
val productId: String,
#ColumnInfo(name = "orderId")
val orderId: String,
#ColumnInfo(name = "quantity")
var quantity: Int,
#ColumnInfo(name = "subtotal")
var subtotal: Double
)
#Entity(tableName = DataConstant.PRODUCT_TABLE)
data class Product(
#PrimaryKey
#NonNull
#ColumnInfo(name = "productId")
val id: String,
#ColumnInfo(name = "name")
var name: String?,
#ColumnInfo(name = "description")
var description: String?,
#ColumnInfo(name = "price")
var price: Double?,
#ColumnInfo(name = "image")
var image: String?,
)
Relations POJOs
data class ProductAndLineItem(
#Embedded val lineItem: LineItem?,
#Relation(
parentColumn = "productId",
entityColumn = "productId"
)
val product: Product?
)
data class OrderWithLineItems(
#Embedded var order: Order,
#Relation(
parentColumn = "orderId",
entityColumn = "orderId",
entity = LineItem::class
)
val lineItemList: List<ProductAndLineItem>
)
Dao
#Dao
interface OrderDao {
#Transaction
#Query("SELECT * FROM `${DataConstant.ORDER_TABLE}` WHERE orderId = :id")
fun getById(id: String): Flow<OrderWithLineItems>
}
Result after running with Dao
Result after running query
Here is my code for entities and dao
You code appears to be fine, with the exception of returning a Flow, testing, using your code, but on the main thread using List (and no WHERE clause) i.e the Dao being :-
#Query("SELECT * FROM ${DataConstant.ORDER_TABLE}")
#Transaction
abstract fun getOrderWithLineItemsAndWithProduct(): List<OrderWithLineItems>
Results in :-
The data being loaded/tested using :-
db = TheDatabase.getInstance(this)
orderDao = db.getOrderDao()
orderDao.clearAll()
orderDao.insert(Product("product1","P1","desc1",10.01,"image1"))
orderDao.insert(Product("product2","P2","desc2",10.02,"image2"))
orderDao.insert(Product("product3","P3","desc3",10.03,"image3"))
orderDao.insert(Product("product4","P4","desc4",10.04,"image4"))
orderDao.insert(Product("","","",0.0,""))
val o1 = orderDao.insert(Order("Order1","initiaited"))
val o2 = orderDao.insert(Order("Order2","finalised")) // Empty aka no List Items
val o1l1 = orderDao.insert(LineItem(10,"product3","Order1",1,10.01))
val o1l2 = orderDao.insert(LineItem(20,"product4","Order1",2,20.08))
val o1l3 = orderDao.insert(LineItem(30,"","Order1",3,30.09))
val o1l4 = orderDao.insert(LineItem(40,"","x",1,10.01))
//val o1l3 = orderDao.insert(LineItem(30,"no such product id","Order1",10,0.0))
// exception whilst trying to extract if not commented out at test = ....
val TAG = "ORDERINFO"
val test = orderDao.getOrderWithLineItemsAndWithProduct()
for(owl: OrderWithLineItems in orderDao.getOrderWithLineItemsAndWithProduct()) {
Log.d(TAG,"Order is ${owl.order.id} status is ${owl.order.status}")
for(pal: ProductAndLineItem in owl.lineItemList) {
Log.d(TAG,"\tLine Item is ${pal.lineItem.id} " +
"for Order ${pal.lineItem.orderId} " +
"for ProductID ${pal.lineItem.productId} " +
"Quantity=${pal.lineItem.quantity} " +
"Product description is ${pal.product.description} Product Image is ${pal.product.image} Price is ${pal.product.price}")
}
}
As such I believe the issue might be that for some reason the Flow is detecting when the first query has completed but prior to the underlying queries.
That is when using #Relation the core objects (Order's) are extracted via the query and the core objects created then the related objects are extracted by a another query and used to build ALL the related objects as a List (unless just the one when it doesn't have to be a list). So prior to this underlying query the core object will have a null or an empty list for the underlying objects. Of course with a hierarchy of #Relations then this is replicated along/down the hierarchy.
I would suggest temporarily adding .allowMainThreadQueires to the databaseBuilder and using a List<OrderWithLineItems> or just a sole OrderWithLineItems. If using this then you get the Product(s) then the issue is with the Flow (which is what I suspect).

Android Room doesn't allow me to do the one-to-N relationship multiple times

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

How to receive not duplicative rows using Room?

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 :-

How can I query a record of part fields using Room in Android Studio with Kotlin?

It works well to query record MVoice using fun listVoice():LiveData<List<MVoice>> with Room framework in Android Studio with Kotlin.
Now I hope to query record of part fields (Such as ID and name) of MVoice, how can I do?
interface DBVoiceDao{
#Query("SELECT * FROM voice_table ORDER BY createdDate desc")
fun listVoice():LiveData<List<MVoice>>
/* How can I do this?
#Query("SELECT id, name FROM voice_table ORDER BY createdDate desc")
fun listVoiceOfPartField():LiveData< ??? >
*/
}
#Entity(tableName = "voice_table", indices = [Index("createdDate")])
data class MVoice(
#PrimaryKey (autoGenerate = true) #ColumnInfo(name = "id") var id: Int = 0,
var name: String = "Untitled",
var path: String = "My path",
var createdDate: Calendar = Calendar.getInstance(),
var isStar: Boolean = false,
var description: String="My description"
)
As "Florina Muntenescu" suggested that Read only what you need in this article 7 Pro-tips for Room
You can achieve it by making a new Model Class:
data class VoiceMinimal(#ColumnInfo(name = "id") val id: Int,
#ColumnInfo(name = "name") val name: String)
In the DAO class, we define the query and select the right columns from the voice table.
#Dao
interface DBVoiceDao {
#Query(“SELECT id, name FROM voice_table ORDER BY createdDate dESC)
fun getVoiceMinimal(): List<VoiceMinimal>
}

Categories

Resources