Im noticing something weird with Room.
I have two entity: Wine and Bottles
A bottle belongs to only one wine, but a wine can have multiple bottles (one to many)
So, i've got the folowing model:
#Entity(tableName = "wine")
data class Wine(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id_wine")
val idWine: Long = 0,
val name: String
)
#Entity(tableName = "bottle")
data class Bottle(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id_bottle")
val idBottle: Long = 0,
#ColumnInfo(name = "id_wine") val idWine: Long,
val comment: String
)
The relation:
data class WineWithBottles (
#Embedded val wine: Wine,
#Relation(
parentColumn = "id_wine",
entityColumn = "id_bottle"
)
val bottles: List<Bottle>
)
And finally there is the database room prepopulate callback:
private val roomCallback: Callback = object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
thread {
val bottleDao = instance?.bottleDao()
val wineDao = instance?.wineDao()
wineDao?.insertWine(Wine(1, "a"))
wineDao?.insertWine(Wine(2, "a"))
wineDao?.insertWine(Wine(3, "a"))
bottleDao?.insertBottle(Bottle(0, 1, a))
bottleDao?.insertBottle(Bottle(0, 1, b))
bottleDao?.insertBottle(Bottle(0, 1, c))
}
}
}
I mention that i provide Wine and Bottles Entities to the Room #Database annotation
So the problem is, when i'am observing a getAllBottles() : LiveData<List<Bottle>>, i get every bottles, everything is fine
But when i'm observing getWineWithBottles(): LiveData<List<WineWithBottles>> i've got one bottle per wine, even though i've set the id_wine of all bottles at 1
Each WineWithBottles object has a wine, and a SINGLE bottle in the list:
[WineWithBottles(wine=Wine(idWine=1, name=a), bottles=[Bottle(idBottle=1, idWine=1, comment=a)]), WineWithBottles(wine=Wine(idWine=2, name=a), bottles=[Bottle(idBottle=2, idWine=1, comment=b)]), WineWithBottles(wine=Wine(idWine=3, name=a), bottles=[Bottle(idBottle=3, idWine=1, comment=c)])]
Try to change relation's condition to wine.id_wine=bottle.id_wine:
data class WineWithBottles (
#Embedded val wine: Wine,
#Relation(
parentColumn = "id_wine",
entityColumn = "id_wine"
)
val bottles: List<Bottle>
)
Related
I have five tables in my database: AREA, AREA_TYPE, SAMPLE, PACK, UNIT
#Entity(tableName = "AREA")
data class AreaEntity(
#PrimaryKey val id:String,
val title:String,
#ColumnInfo(name = "area_type_id") val areaTypeId:Int,
#ColumnInfo(name = "is_active") val isActive:Boolean
)
#Entity(tableName = "AREA_TYPE")
data class AreaTypeEntity(
#PrimaryKey val id:String,
val title:String,
#ColumnInfo(name = "title") val parentAreaId : String
)
#Entity(tableName = "SAMPLE")
data class SampleEntity(
#PrimaryKey val id:String,
val title:String,
)
#Entity(tableName = "PACK")
data class PackEntity(
#PrimaryKey val id:String,
val title:String,
)
#Entity(tableName = "UNIT")
data class UnitEntity(
#PrimaryKey val id:String,
#ColumnInfo(name = "sample_id") val parentAreaId : String,
#ColumnInfo(name = "area_id") val areaId:Int,
#ColumnInfo(name = "pack_type_id") val packTypeId: Int,
#ColumnInfo(name = "is_active") val isActive:Boolean
)
UNIT table has three foreign keys : sample_id, area_id, pack_id
Every area has one-to-one relationship with area type.
I have an AreaPOJO for Area-AreaType Relationship:
data class AreaPOJO (
#Embedded val areaEntity : AreaEntity
#Relation (
parentColumn = "area_id",
entityColumn = "id"
)
val areaTypeEntity : AreaTypeEntity
)
Visual view of tables (https://i.stack.imgur.com/bXzl5.png)
So I assume that I will have a POJO for UNIT for the Relationships like this:
data class UnitPOJO (
#Embedded val unitEntity : UnitEntity
#Relation (
parentColumn = "area_id",
entityColumn = "id"
)
val areaEntity : AreaEntity
#Relation (
parentColumn = "pack_id",
entityColumn = "id"
)
val packEntity : PackEntity
#Relation (
parentColumn = "sample_id",
entityColumn = "id"
)
val sampleEntity : SampleEntity
)
With this POJO, I can get AreaEntity,SampleEntity,UnitEntity but I can't get AreaTypeEntity for UnitPOJO.
When I use AreaPOJO instead of AreaEntity, I have a compilation error which tells me to use "prefix" for AreaPOJO. When I use prefix, this time AreaPOJO gives an error that it can't find the column names for relationship.
So I am stuck :)
Briefly I need all the fields from all five tables for this query :
"SELECT * FROM UNIT
INNER JOIN AREA ON UNIT.AREA_ID = AREA.ID
INNER JOIN AREA_TYPE ON AREA.AREA_TYPE_ID = AREA_TYPE.ID
INNER JOIN SAMPLE ON UNIT.SAMPLE_ID = SAMPLE.ID
INNER JOIN PACK ON UNIT.PACK_ID = PACK.ID"
First the use of prefix, this is an option to circumvent the ambiguous column names (e.g. which id column is the correct one to use? (rhetorical)) BUT you would then have to play around with the queries to include AS (implicitly or explicitly) to rename the extracted columns.
I would suggest that using unique column names is the way to avoid such ambiguities.
Onto the grandparent/grandchild.
In short you are close BUT you retrieve an AreaPOJO (Area with Type) not an AreaEntity, but you then have to tell Room to use the AreaEntity class (as that is the class used to ascertain the columns for the AreaEntity and then the #relation in the AreaPOJO knows to get the inderlying AreaType).
So although untested but successfully compiled consider the following:-
#Entity(tableName = "UNIT")
data class UnitEntity(
#PrimaryKey val id:String,
#ColumnInfo(name = "sample_id") val parentAreaId : String,
#ColumnInfo(name = "area_id") val areaId:Int,
#ColumnInfo(name = "pack_type_id") val packTypeId: Int,
#ColumnInfo(name = "is_active") val isActive:Boolean
)
#Entity(tableName = "AREA_TYPE")
data class AreaTypeEntity(
#PrimaryKey #ColumnInfo(name = "area_type_id") val id:String, //<<<<< unique name
val title:String,
#ColumnInfo(name = "area_type_title") val parentAreaId : String
)
data class AreaPOJO(
#Embedded val areaEntity : AreaEntity,
#Relation(
parentColumn = "area_type_id", //<<<<< changed accrodingly
entityColumn = "area_type_id" //<<<<< changed accordingly
)
val areaTypeEntity : AreaTypeEntity
)
data class UnitPOJO (
#Embedded val unitEntity : UnitEntity,
#Relation (
entity = AreaEntity::class, //<<<<< ADDED
parentColumn = "area_id",
entityColumn = "area_id"
)
val areaWithAreaType : AreaPOJO,
#Relation (
parentColumn = "pack_type_id",
entityColumn = "pack_id"
)
val packEntity : PackEntity,
#Relation (
parentColumn = "sample_id",
entityColumn = "sample_id"
)
val sampleEntity : SampleEntity
)
Notice the pack_type_id and pack_id in the UnitPOJO as opposed to sample_id and sample_id for the sample reference/relationship.
I would suggest considering using unique names e.g. pack_type_id is referencing/mapping the relationship between the unit and the pack, perhaps naming the column pack_id_map in the Unit. Thus the column names are then more descriptive and also unique. The downside is that there is more coding.
using the above the #Query could be :-
#Transaction
#Query("SELECT * FROM UNIT")
fun getUnitsWithRelations(): List<UnitPOJO>
Obviously adjusted according if using Flow/Live Data and so on.
Saying that, the above is inefficient as when Room processes an #Relation it builds and underlying query per #Relation that gets ALL the children from the parent (I believe on a per parent basis). In your case, it appears that you have 1 to many relationships thus #Embedded can be used BUT the query has to be more complex.
Working Example
The following is a working example based upon your code that
uses both #Relation and #Embedded resolutions
The #Relation POJO's are UnitPOJO and AreaPOJO
The #Embedded versions are prefixed with Alternative
Adds data (3 rows into each table, except 5 Units)
Extracts the Unit's and the related data using both alternatives
includes Foreign Key constraints that enforce referential and maintain integrity see https://sqlite.org/foreignkeys.html
It should be noted that some changes have been made as I believe that you some unusual and at a guess unecesassry relationships. e.g. You appear to have Area relate to AreaType both ways when only one is required. That is an Area will have an AreaType as a parent but if an ArearType also has an Area as a parent then you get the chicken and egg scenario.
The assumption has been made that an Area has one of the many available AreaTypes as a parent.
First the classes (see comments) :-
#Entity(
tableName = "AREA",
/* Enforces/Maintains referential Integrity */
/* i.e does not allow orphans */
foreignKeys = [
ForeignKey(
entity = AreaTypeEntity::class,
parentColumns = ["area_type_id"],
childColumns = ["area_type_id_map" ],
onDelete = ForeignKey.CASCADE /* ????? */,
onUpdate = ForeignKey.CASCADE /* ????? */
)
]
)
data class AreaEntity(
#PrimaryKey #ColumnInfo(name = "area_id")val id:String, //<<<<< unique name
#ColumnInfo(name = "area_title") val title:String,
#ColumnInfo(name = "area_type_id_map") val areaTypeId:String, //<<<<< see Area Type
#ColumnInfo(name = "area_is_active") val isActive:Boolean
)
#Entity(tableName = "SAMPLE")
data class SampleEntity(
#PrimaryKey #ColumnInfo(name = "sample_id") val id:String, //<<<<< unique name
#ColumnInfo(name = "sample_title") val title:String,
)
#Entity(tableName = "PACK")
data class PackEntity(
#PrimaryKey #ColumnInfo(name = "pack_id") val id:String, //<<<<< unique name
#ColumnInfo(name = "pack_title") val title:String, //<<<<< unique name
)
#Entity(
tableName = "UNIT",
foreignKeys = [
ForeignKey(
entity = SampleEntity::class,
parentColumns = ["sample_id"],
childColumns = ["sample_id_map"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = AreaEntity::class,
parentColumns = ["area_id"],
childColumns = ["area_id_map"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = PackEntity::class,
parentColumns = ["pack_id"],
childColumns = ["pack_id_map"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class UnitEntity(
#PrimaryKey val id:String,
#ColumnInfo(name = "sample_id_map") val sampleId : String,
#ColumnInfo(name = "area_id_map") val areaId:String,
#ColumnInfo(name = "pack_id_map") val packTypeId: String,
#ColumnInfo(name = "unit_is_active") val isActive:Boolean
)
#Entity(
tableName = "AREA_TYPE"
)
data class AreaTypeEntity(
#PrimaryKey #ColumnInfo(name = "area_type_id") val id:String, //<<<<< unique name
#ColumnInfo(name = "area_type_title") val title:String,
/* ???? should an area type have an area as a parent? potential issues if so */
/* commented out
#ColumnInfo(name = "area_type_title") val parentAreaId : String //<<<<< unique name
*/
)
data class AreaPOJO(
#Embedded val areaEntity : AreaEntity,
#Relation(
parentColumn = "area_type_id_map", //<<<<< changed accordingly
entityColumn = "area_type_id" //<<<<< changed accordingly
)
val areaTypeEntity : AreaTypeEntity
)
data class UnitPOJO (
#Embedded val unitEntity : UnitEntity,
#Relation (
entity = AreaEntity::class, //<<<<< ADDED
parentColumn = "area_id_map",
entityColumn = "area_id"
)
val areaWithAreaType : AreaPOJO,
#Relation (
parentColumn = "pack_id_map",
entityColumn = "pack_id"
)
val packEntity : PackEntity,
#Relation (
parentColumn = "sample_id_map",
entityColumn = "sample_id"
)
val sampleEntity : SampleEntity
)
data class AlternativeAreaPOJO (
#Embedded val areaEntity: AreaEntity,
#Embedded val areaTypeEntity: AreaTypeEntity
)
data class AlternativeUnitPOJO (
#Embedded val unitEntity: UnitEntity,
#Embedded val alternativeAreaPOJO: AlternativeAreaPOJO,
#Embedded val packEntity: PackEntity,
#Embedded val sampleEntity: SampleEntity
)
The #Dao annotated interface AllDao :-
#Dao
interface AllDAO {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(areaEntity: AreaEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(unitEntity: UnitEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(sampleEntity: SampleEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(packEntity: PackEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(areaTypeEntity: AreaTypeEntity)
#Transaction
#Query("SELECT * FROM UNIT")
fun getUnitsWithRelations(): List<UnitPOJO>
#Query("SELECT * FROM UNIT " +
"INNER JOIN AREA ON UNIT.area_id_map = AREA.area_id " +
"INNER JOIN AREA_TYPE ON AREA.area_type_id_map = AREA_TYPE.area_type_id " +
"INNER JOIN SAMPLE ON UNIT.sample_id_map = SAMPLE.sample_id " +
"INNER JOIN PACK ON UNIT.pack_id_map = PACK.pack_id")
fun getAlternativeUnitsWithRelations(): List<AlternativeUnitPOJO>
}
The #Database annotated class TheDatabase :-
#Database(entities = [
AreaEntity::class,
SampleEntity::class,
PackEntity::class,
UnitEntity::class,
AreaTypeEntity::class
],
version = 1,
exportSchema = false
)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDAO(): AllDAO
companion object {
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
context,
TheDatabase::class.java,
"the_database.db"
)
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
Note for convenience and brevity .allowMainThreadQueries has been utilised.
Code within an activity (designed to run just the once):-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDAO()
val TAG = "DBINFO"
val p1 = PackEntity("P001","Pack1")
val p2 = PackEntity("P002","Pack2")
val p3 = PackEntity("P003","Pack3")
dao.insert(p1)
dao.insert(p2)
dao.insert(p3)
val s1 = SampleEntity("S001","Sample1")
val s2 = SampleEntity("S002","Sample2")
val s3 = SampleEntity("S003","Sample3")
dao.insert(s1)
dao.insert(s2)
dao.insert(s3)
val at1 = AreaTypeEntity("AT001","AreaType1")
val at2 = AreaTypeEntity("AT002","AreaType2")
val at3 = AreaTypeEntity("AT003","AreaType3",)
dao.insert(at1)
dao.insert(at2)
dao.insert(at3)
val a1 = AreaEntity("A001","Area1",at1.id,true)
val a2 = AreaEntity("A002","Area2",at2.id,false)
val a3 = AreaEntity("A003","Area3",at1.id,true)
dao.insert(a1)
dao.insert(a2)
dao.insert(a3)
dao.insert(UnitEntity("U001",s1.id,a1.id,p1.id,true))
dao.insert(UnitEntity("U002",s2.id,a2.id,p2.id, false))
dao.insert(UnitEntity("U003",s3.id,a3.id,p3.id,true))
dao.insert(UnitEntity("U004",s1.id,a2.id,p3.id,false))
dao.insert(UnitEntity("U005",s3.id,a2.id,p1.id, true))
for(uwr in dao.getUnitsWithRelations()) {
Log.d(TAG,
"Unit is ${uwr.unitEntity.id} " +
"Active = ${uwr.unitEntity.isActive} " +
"Sample is ${uwr.sampleEntity.title} " +
"Area is ${uwr.areaWithAreaType.areaEntity.title} " +
"AreaType is ${uwr.areaWithAreaType.areaTypeEntity.title}"
)
}
for (auwr in dao.getAlternativeUnitsWithRelations()) {
Log.d(TAG,
"Unit is ${auwr.unitEntity.id} " +
"Active is ${auwr.unitEntity.isActive} " +
"Sample is ${auwr.sampleEntity.title} " +
"Area is ${auwr.alternativeAreaPOJO.areaEntity.title} " +
"AreaType is ${auwr.alternativeAreaPOJO.areaTypeEntity.title}"
)
}
}
}
Last the resultant output from the log:-
2022-04-05 09:32:40.528 D/DBINFO: Unit is U001 Active = true Sample is Sample1 Area is Area1 AreaType is AreaType1
2022-04-05 09:32:40.528 D/DBINFO: Unit is U002 Active = false Sample is Sample2 Area is Area2 AreaType is AreaType2
2022-04-05 09:32:40.529 D/DBINFO: Unit is U003 Active = true Sample is Sample3 Area is Area3 AreaType is AreaType1
2022-04-05 09:32:40.529 D/DBINFO: Unit is U004 Active = false Sample is Sample1 Area is Area2 AreaType is AreaType2
2022-04-05 09:32:40.529 D/DBINFO: Unit is U005 Active = true Sample is Sample3 Area is Area2 AreaType is AreaType2
2022-04-05 09:32:40.537 D/DBINFO: Unit is U001 Active is true Sample is Sample1 Area is Area1 AreaType is AreaType1
2022-04-05 09:32:40.537 D/DBINFO: Unit is U002 Active is false Sample is Sample2 Area is Area2 AreaType is AreaType2
2022-04-05 09:32:40.537 D/DBINFO: Unit is U003 Active is true Sample is Sample3 Area is Area3 AreaType is AreaType1
2022-04-05 09:32:40.537 D/DBINFO: Unit is U004 Active is false Sample is Sample1 Area is Area2 AreaType is AreaType2
2022-04-05 09:32:40.537 D/DBINFO: Unit is U005 Active is true Sample is Sample3 Area is Area2 AreaType is AreaType2
i.e. the results are the same for the 2 alternatives and of course the relationships are working as expected
Actually, I have solved it by this POJO :
data class UnitPOJO
(
#Embedded val unit: UnitEntity,
#Relation(
parentColumn = "sample_id",
entityColumn = "id"
)
val sampleEntity: SampleEntity,
#Relation(
parentColumn = "pack_type_id",
entityColumn = "id"
)
val pack: PackEntity,
#Relation(
parentColumn = "area_id",
entityColumn = "id",
entity = AreaEntity::class
)
val area: AreaPOJO
)
and AreaPOJO like this :
data class AreaPOJO(
#Embedded val areaEntity: AreaEntity,
#Relation(
parentColumn = "area_type_id",
entityColumn = "id"
)
val areaTypeEntity: AreaTypeEntity
)
But I will definitely consider your warnings #MikeT about naming the fields/column names when using hierarchical data. If something goes wrong or I get unexpected results, I will definitely use this approach to solve the problem.
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).
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 need to triple join my entities using #relation anotion in room, but I don't know how.
here is my summary of entities:
#Entity(tableName = "session_table")
data class Session(
#PrimaryKey(autoGenerate = true)
var sessionId: Long = 0L,
#ColumnInfo(name = "lesson_id")
var lessonId: Long
)
#Entity(tableName = "lessons_table")
data class Lesson(
#PrimaryKey(autoGenerate = true)
val lessonId: Long,
#ColumnInfo(name = "teacher_id")
var teacherId: Long = -1L
)
#Entity(tableName = "teacher_table")
data class Teacher(
#PrimaryKey(autoGenerate = true)
val teacherId: Long = 0L
)
I assume the answer would be something like this:
data class SessionWithLessonWithTeacher(
#Embedded
val session: Session,
#Relation(
parentColumn = "lesson_id",
entityColumn = "lessonId"
)
var lesson: Lesson,
#Relation(
parentColumn = "teacher_id", // this is the teacher id in lesson
entityColumn = "teacherId",
)
var teacher: Teacher
)
Your guess at what SessionWithlessonWithTeacher should be won't work because it is effectively saying get the teacher_id from the Session. There is no teacher_id in a Session.
To use #Relation's then you need to follow the hierarchy. A Session has a Lesson and a Lesson has a Teacher. So in Session you need to get a Lesson with a Teacher.
As such have (working up though the hierarchy) :-
data class LessonWithTeacher(
#Embedded
val lesson: Lesson,
#Relation(
entity = Teacher::class,
parentColumn = "teacher_id",
entityColumn = "teacherId")
val teacher: Teacher
)
and
data class SessionWithLessonWithTeacher(
#Embedded
val session: Session,
#Relation(
entity = Lesson::class, /* NOTE Lesson NOT LessonWithTeacher (not a table) */
parentColumn = "lesson_id",
entityColumn = "lessonId")
val lessonWithTeacher: LessonWithTeacher
)
You would use an #Dao such as :-
#Query("SELECT * FROM session_table")
#Transaction
abstract fun getSessionWithLessonWithTeacher(): List<SessionWithLessonWithTeacher>
Alternative Approach
The way that Room uses #Relation is that it initially only retrieves the Embedded object, it then retrieves all of the #Related objects via subsequent queries and hence why it warns(expects) #Transaction.
For your scenario, where there will be 1 lesson per session and 1 teacher per lesson, you can embed all three and have a single query (albeit it more complicated) that JOIN's the three tables.
So instead of (or as well as) SessionWithLessonWithTeacher you could have:-
data class SessionLessonTeacher(
#Embedded
val session: Session,
#Embedded
val lesson: Lesson,
#Embedded
val teacher: Teacher
)
note #Embedded can be a pain if column names aren't unique between the embedded objects
The equivalent query would/could be :-
#Query("SELECT * FROM session_table " +
"JOIN lessons_table ON lessons_table.lessonId = session_table.lesson_id " +
"JOIN teacher_table ON teacher_table.teacherId = lessons_table.teacher_id")
abstract fun getSessionLessonTeacher(): List<SessionLessonTeacher>
note that in your case the table.column could just be column as the column names are all unique.
I am having a relational data structure like the following
Entry.kt
#Entity(tableName = "entries")
data class Entry(
#ColumnInfo(name = "entry_id") #PrimaryKey(autoGenerate = true) val entryID: Long,
#ColumnInfo(name = "time_stamp") val timeStamp: Timestamp
)
Feeling.kt
#Entity(tableName = "feelings")
data class Feeling(
#ColumnInfo(name = "feeling_id") #PrimaryKey(autoGenerate = true) val feelingID: Int,
val feeling: String,
val color: Int
)
Subentry.kt
#Entity(tableName = "sub_entries",indices = [Index("feeling_id")])
data class SubEntry(
#ColumnInfo(name = "sub_entry_id") #PrimaryKey(autoGenerate = true) val subEntryID: Long,
#ColumnInfo(name = "entry_id") var entryID: Long,
#ColumnInfo(name = "feeling_id") val feelingID: Int,
val intensity: Int
)
as well as a DOJO linking the three
EntryWithSubEntriesAndFeelings.kt
data class EntryWithSubEntriesAndFeelings(
#Embedded val entry: Entry,
#Relation(
parentColumn = "entry_id",
entityColumn = "entry_id"
)
val subEntries: List<SubEntry>,
#Relation(
parentColumn = "entry_id",
entityColumn = "feeling_id",
associateBy = Junction(SubEntry::class)
)
val feelings: List<Feeling>
)
Now with a prepopulated feelings database, everything works fine. But I am currently adding the feature to insert custom feelings. Those and the corresponding subentries end up correctly in the subentry database (I copied the database files from the virtual device and checked them manually). But for some reason, the new feeling is not getting passed to the observer and the adapter and thus it is not being displayed together with the other subentries.
Any hints are appreciated! :)
Turns out I still had some hardcoded references to the initial list in my code which prevented the new feeling from being displayed.