3 way intersection Table in room Android - android

I am trying to achieve the following
I have the following Entities
#Entity(tableName = "workspace_table")
data class WorkSpace(
#PrimaryKey
val workSpaceId:Long,
.....
)
#Entity(tableName = "widget_table")
data class Widget(
val widgetId:Long,
.......
)
#Entity(tableName = "feed_table")
data class Feed(
val feedId:Long,
.......
)
What I want from the 3 table is the below
POJO
data class MergedData(
#Embedded workSpace:WorkSpace,
#Embedded widget:List<Widget>,
#Embedded feeds:List<Feed>,
)
The relationship is like this
workSpaceId|widgetId|feedId|
1 | 1 | 2
1 | 1 | 1 |
2 | 1 | 2
2 | 2 | 1
Basically there is a many to many relation ship between workspace and widgets and widgets and feeds
They should come together when all of the three tables are participating
I went through the guide
https://developer.android.com/training/data-storage/room/relationships
and tried Mapping two way between widget and workspace and feed and widget
however those I am not even been able to build with that
I tried one to many with Workspace and Widget and Many to Many with Widget and Feeds
then I am getting feeds in my workspace for widgets which I don't want .
I am really confused at this point any nudge in the correct direction will greatly appreciated
Update
With Mike's Answer below I am getting this
2021-04-04 12:16:06.097 10237-10291/com.example.datacreation D/MainActivty: meta data [IntersectionWithWorkSpaceWidgetFeed(workSpace=WorkSpace(workSpaceId=2,
associatedUserId=test, workSpaceName=Demo),
widget=WidgetMetaData(widgetId=11, widgetName=Widget1, backgroundColor=None, widgetType=Normal, dataUrl=www),
feed=Feed(feedId=2, feedName=Feed2, count=0, asyncInterval=1234)),
IntersectionWithWorkSpaceWidgetFeed(workSpace=WorkSpace(workSpaceId=2, associatedUserId=test,
workSpaceName=Demo),
widget=WidgetMetaData(widgetId=12, widgetName=Widget2, backgroundColor=None, widgetType=normal, dataUrl=www),
feed=Feed(feedId=1, feedName=Feed1, count=2, asyncInterval=1234)),
IntersectionWithWorkSpaceWidgetFeed(workSpace=WorkSpace(workSpaceId=2, associatedUserId=igvuser, workSpaceName=Demo),
widget=WidgetMetaData(widgetId=13, widgetName=Widget3, backgroundColor=None, widgetType=normal, dataUrl=www),
feed=Feed(feedId=2, feedName=Feed2, count=0, asyncInterval=1234))]
Near enough to my original MergedData POJO. Thanks Mike.

I believe that you want a 3 way mapping table. Each row consisting of 3 columns, the WorkSpaceId, the WidgetId and the FeedId with the Primary Key composed of all 3 columns.
Assuming this here is a working example:-
The 3 base entities:
WorkSpace
#Entity(
tableName = "workspace_table",
)
data class WorkSpace(
#PrimaryKey
val workSpaceId:Long,
val workPlaceName: String
)
Widget
#Entity(
tableName = "widget_table",
)
data class Widget(
#PrimaryKey
val widgetId:Long,
val widgetName: String
)
Feed
#Entity(tableName = "feed_table")
data class Feed(
#PrimaryKey
val feedId:Long,
val feedName: String
)
The NEW mapping table WorkSpaceWidgetFeedIntersectionMap
#Entity(
tableName = "workspace_widget_feed_mapping_table",
foreignKeys = [
ForeignKey(
entity = WorkSpace::class,
parentColumns = ["workSpaceId"],
childColumns = ["workSpaceId_map"]
),
ForeignKey(
entity = Widget::class,
parentColumns = ["widgetId"],
childColumns = ["widgetId_map"]
),
ForeignKey(
entity = Feed::class,
parentColumns = ["feedId"],
childColumns = ["feedId_map"]
)
],
primaryKeys = ["workSpaceId_map","widgetId_map","feedId_map"],
)
data class WorkSpaceWidgetFeedIntersectionMap(
#NonNull
val workSpaceId_map: Long,
#NonNull
val widgetId_map: Long,
#NonNull
val feedId_map: Long
)
Foreign Keys are optional
The Dao's AllDao
#Dao
interface AllDao {
#Insert
fun insertWorkSpace(workSpace: WorkSpace): Long
#Insert
fun insertWidget(widget: Widget): Long
#Insert
fun insertFeed(feed: Feed): Long
#Insert
fun insertWorkSpaceWidgetFeedMap(workSpaceWidgetFeedIntersectionMap: WorkSpaceWidgetFeedIntersectionMap): Long
#Query("DELETE FROM workspace_table")
fun clearWorkSpaceTable(): Int
#Query("DELETE FROM widget_table")
fun clearWidgetTable(): Int
#Query("DELETE FROM feed_table")
fun clearFeedTable(): Int
#Query("DELETE FROM workspace_widget_feed_mapping_table")
fun clearWorkSpaceWidgetFeedMap(): Int
#Query("SELECT * FROM workspace_widget_feed_mapping_table")
fun getWorkSpaceWidgetFeedIntersections(): List<WorkSpaceWidgetFeedIntersectionMap>
}
The Database MyDatabase
#Database(entities = [WorkSpace::class,Widget::class,Feed::class,WorkSpaceWidgetFeedIntersectionMap::class],version = 1)
abstract class MyDatabase: RoomDatabase() {
abstract fun getAllDoa(): AllDao
}
And finally an Activity MainActivity to test the basic functionality
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val db = Room.databaseBuilder(applicationContext,MyDatabase::class.java,"mydb")
.allowMainThreadQueries()
.build()
val dao = db.getAllDoa()
clearAllTables(dao)
dao.insertWorkSpace(WorkSpace(1,"WorkSpace 1"))
dao.insertWorkSpace( WorkSpace(2,"WorkSpace 2"))
dao.insertWidget(Widget(1,"Widget 1"))
dao.insertWidget(Widget(2,"Widget 2"))
dao.insertFeed(Feed(1,"Feed 1"))
dao.insertFeed( Feed(2,"Feed 2"))
dao.insertWorkSpaceWidgetFeedMap(WorkSpaceWidgetFeedIntersectionMap(1,1,2))
dao.insertWorkSpaceWidgetFeedMap(WorkSpaceWidgetFeedIntersectionMap(1,1,1))
dao.insertWorkSpaceWidgetFeedMap(WorkSpaceWidgetFeedIntersectionMap(2,1,2))
dao.insertWorkSpaceWidgetFeedMap(WorkSpaceWidgetFeedIntersectionMap(2,2,1))
val wwfiList = dao.getWorkSpaceWidgetFeedIntersections()
for(cwwfi: WorkSpaceWidgetFeedIntersectionMap in wwfiList) {
Log.d("WWFIINFO","WorkSpaceID = " + cwwfi.workSpaceId_map + " WidgetID = " + cwwfi.widgetId_map + " FeedID = " + cwwfi.feedId_map)
}
}
private fun clearAllTables(dao: AllDao) {
dao.clearWorkSpaceWidgetFeedMap()
dao.clearFeedTable()
dao.clearWidgetTable()
dao.clearWorkSpaceTable()
}
}
gets the built the database (allowing to be run on main thread for convenience and brevity)
gets the dao
clears all the tables (makes test rerunnable)
adds 2 WorkSpaces, 2 Widgets and 2 Feeds
adds the intersection map entries
extracts and logs the intersections
Result
Running the above produces :-
2021-04-04 08:31:02.942 D/WWFIINFO: WorkSpaceID = 1 WidgetID = 1 FeedID = 2
2021-04-04 08:31:02.942 D/WWFIINFO: WorkSpaceID = 1 WidgetID = 1 FeedID = 1
2021-04-04 08:31:02.942 D/WWFIINFO: WorkSpaceID = 2 WidgetID = 1 FeedID = 2
2021-04-04 08:31:02.943 D/WWFIINFO: WorkSpaceID = 2 WidgetID = 2 FeedID = 1
You could easily then get the respective WorkSpace, Wdiget and Feed from the retrieved WorkSpaceWidgetFeedIntersectionMap.
see addtional
Additional
Now to get your MergedData (equivalent) then consider the following additions to the above
New data class IntersectionWithWorkSpaceWidgetFeed
:-
class IntersectionWithWorkSpaceWidgetFeed(
#Embedded
val workSpace: WorkSpace,
#Embedded
val widget: Widget,
#Embedded
val feed: Feed
)
An extra Dao function getWorkSpaceWidgetAndFeedFromIntersectionMap()
:-
#Query("SELECT * FROM workspace_widget_feed_mapping_table JOIN workspace_table ON workSpaceId = workSpaceId_map JOIN widget_table ON widgetId = widgetId_map JOIN feed_table ON feedId = feedId_map")
fun getWorkSpaceWidgetAndFeedFromIntersectionMap(): List<IntersectionWithWorkSpaceWidgetFeed>
A new (or replace existing 6.) section in MainActivity's onCreate method
:-
val iwwfList= dao.getWorkSpaceWidgetAndFeedFromIntersectionMap()
for(iwwf: IntersectionWithWorkSpaceWidgetFeed in iwwfList) {
Log.d("WWFINFO","WorkSpaceID = " + iwwf.workSpace.workSpaceId + " WorkSpaceName = " + iwwf.workSpace.workPlaceName +
" WidgetID = " + iwwf.widget.widgetId + " WidgetName = " + iwwf.widget.widgetName +
" FeedID = " + iwwf.feed.feedId + " FeedName = " + iwwf.feed.feedName
)
}
The Result from the above changes :-
2021-04-04 09:20:34.371 D/WWFINFO: WorkSpaceID = 1 WorkSpaceName = WorkSpace 1 WidgetID = 1 WidgetName = Widget 1 FeedID = 2 FeedName = Feed 2
2021-04-04 09:20:34.371 D/WWFINFO: WorkSpaceID = 1 WorkSpaceName = WorkSpace 1 WidgetID = 1 WidgetName = Widget 1 FeedID = 1 FeedName = Feed 1
2021-04-04 09:20:34.371 D/WWFINFO: WorkSpaceID = 2 WorkSpaceName = WorkSpace 2 WidgetID = 1 WidgetName = Widget 1 FeedID = 2 FeedName = Feed 2
2021-04-04 09:20:34.371 D/WWFINFO: WorkSpaceID = 2 WorkSpaceName = WorkSpace 2 WidgetID = 2 WidgetName = Widget 2 FeedID = 1 FeedName = Feed 1

Related

Update Query in Child Table won't relect in parent table Android Room Kotlin

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

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

Room database with one-to-one relation like Address, City and State

I looked in the android documentation for an answer to my question, but I couldn't find it. To create a recyclerview using the information contained in these classes, how can I get a list of this information in Room
#Entity(
foreignKeys = [
ForeignKey(
entity = City::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("cityfk"),
onDelete = ForeignKey.NO_ACTION
)
]
)
data class Address(
#PrimaryKey
#ColumnInfo
var id: Long = 0
) : Serializable {
#ColumnInfo
var name: String = ""
#ColumnInfo(index = true)
var cityfk: Long = 0
}
#Entity(
foreignKeys = [
ForeignKey(
entity = State::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("statefk"),
onDelete = ForeignKey.NO_ACTION
)
]
)
data class City(
#PrimaryKey
#ColumnInfo
var id: Long = 0
) : Serializable {
#ColumnInfo
var name: String = ""
#ColumnInfo(index = true)
var statefk: Long = 0
}
#Entity
data class State(
#PrimaryKey
#ColumnInfo
var id: Long = 0
) : Serializable {
#ColumnInfo
var name: String = ""
}
How can I get a list of addresses listing the classes?
How to get a result like this in ANSI SQL:
select ADDRESS.NAME ADDRESS
, CITY.NAME CITY
, STATE.NAME STATE
from ADDRESS
join CITY
on CITY.ID = ADDRES.CITYFK
join STATE
on STATE.ID = CITY.STATEFK
You would typically have a POJO to represent the combined data. You can then either have a field/variable for the extracted columns noting that values are matched to the liked named variable.
You can use #Embedded to include an entity in it's entirety so in theory embed Address City and State.
see variable/column name issues
You can use #Embedded along with #Relation for the child (children) BUT not for grandchildren (e.g. State). You would need an underlying City with State POJO where City is embedded and State is related by an #Relation.
variable/column names are not an issue when using #Relation as room builds underlying queries from the parent.
Variable/Column name issues
Room maps columns to variable according to variable names. So there will be issues with id's and name columns if using the simpler #Embedded for all three entities.
I would suggest always using unique names e.g. addressId, cityId, StateId, (at least for the column names e.g. #ColumnInfo(name = "addressId")) but simpler to just have var addressid.
An alternative is the use the #Embedded(prefix = "the_prefix") on some, this tells room to match the variable to column name with the prefix so you need to use AS in the SQL. Obviously the_prefix would be changed to suit.
The Dao's
if using #Embedded with #Relation then you simply need to get the parent so
#Query("SELECT * FROM address")
fun getAddressWithCityAndWithState(): List<AddressWithCityAndWithState>
where AddressWithCityAndWithState is the POJO that has the Address #Embedded and the CityWithState with #Relation.
You would also need the accompanying CityWithState POJO with City #Embedded and State with #Relation.
If Embedding Address, City and State with City having a prefix of "city_" and state having a prefix of "state_" then you would use something like :-
#Query("SELECT address.*, city.id AS city_id, city.name AS city_name, state.id AS state_id, state.name AS state_name FROM address JOIN city ON address.cityfk = city.it JOIN state ON city.statefk = state.id")
fun getAddressWithCityAndWithState(): List<AddressWithCityAndWithState>
where AddressWithCityAndWithState is the POJO that has Address, City and State #Embedded
Note the above is in-principle.
Working Example
The following is a working example based upon
a) renaming the columns to avoid ambiguity and
b) using #Embedded of all three classes in the POJO AddressWithCityWithState
First changes to the Address, City and State to rename the columns :-
Address :-
#Entity(
foreignKeys = [
ForeignKey(
entity = City::class,
parentColumns = arrayOf("city_id"), //<<<<<<<<<< CHANGED
childColumns = arrayOf("cityfk"),
onDelete = ForeignKey.NO_ACTION
)
]
)
data class Address(
#PrimaryKey
#ColumnInfo(name ="address_id") //<<<<<<<<<< ADDED name
var id: Long = 0
) : Serializable {
#ColumnInfo(name = "address_name") //<<<<<<<<<< ADDDED name
var name: String = ""
#ColumnInfo(index = true)
var cityfk: Long = 0
}
City :-
#Entity(
foreignKeys = [
ForeignKey(
entity = State::class,
parentColumns = arrayOf("state_id"), //<<<<<<<<<< changed
childColumns = arrayOf("statefk"),
onDelete = ForeignKey.NO_ACTION
)
]
)
data class City(
#PrimaryKey
#ColumnInfo(name = "city_id") // <<<<<<<<<< ADDED name
var id: Long = 0
) : Serializable {
#ColumnInfo(name = "city_name") //<<<<<<<<<< ADDED name
var name: String = ""
#ColumnInfo(index = true)
var statefk: Long = 0
}
State :-
#Entity
data class State(
#PrimaryKey
#ColumnInfo(name = "state_id") // ADDED name
var id: Long = 0
) : Serializable {
#ColumnInfo(name = "state_name") // ADDED name
var name: String = ""
}
Next the POJO AddressWithCityWithState :-
data class AddressWithCityWithState (
#Embedded
val address: Address,
#Embedded
val city: City,
#Embedded
val state: State
)
due to unique column names no prefix = ? required
A suitable DAO :-
#Query("SELECT * FROM address JOIN city on address.cityfk = city.city_id JOIN state ON city.statefk = state.state_id")
fun getAllAddressesWithCityAndWithState(): List<AddressWithCityWithState>
simplified due to column renaming so * instead AS clauses for ambiguous column names
Using the above :-
allDao = db.getAllDao()
var state = State()
state.name = "State1"
var stateid = allDao.insert(state)
var city = City()
city.name = "City1"
city.statefk = stateid
var cityid = allDao.insert(city)
var address = Address()
address.name = "Address1"
address.cityfk = cityid
allDao.insert(address)
for(awcws: AddressWithCityWithState in allDao.getAllAddressesWithCityAndWithState()) {
Log.d("DBINFO","${awcws.address.name}, ${awcws.city.name}, ${awcws.state.name}")
}
The result in the log being :-
2021-11-22 07:43:28.574 D/DBINFO: Address1, City1, State1
Other working examples (without changing column names)
Without any changes to the Entities (Address, city and state). Here are working examples of the other options.
1- Get full address as a single string, all that is required is the query such as :-
#Query("SELECT address.name||','||city.name||','||state.name AS fullAddress FROM address JOIN city ON address.cityfk = city.id JOIN state ON city.statefk = state.id ")
fun getAddressesAsStrings(): List<String>
of course not much use say for a drop down selector as you can't ascertain where in the database the rows came from.
2 - Basic POJO with unambiguous column names
The POJO :-
data class AddressWithCityWithState(
var address_id: Long,
var address_name: String,
var city_id: Long,
var city_name: String,
var state_id: Long,
var state_name: String
)
The query :-
/*
* Returns multiple columns renamed using AS clause to disambiguate
* requires POJO with matching column names
* */
#Query("SELECT " +
"address.id AS address_id, address.name AS address_name, " +
"city.id AS city_id, city.name AS city_name, " +
"state.id AS state_id, state.name AS state_name " +
"FROM address JOIN city ON address.cityfk = city.id JOIN state ON city.statefk = state.id")
fun getAddressesWithCityAndStateViaBasicPOJO(): List<AddressWithCityWithState>
3- POJO using EMBEDS
The POJO :-
data class AddressWithCityWithStateViaEmbeds(
#Embedded
var address: Address,
#Embedded(prefix = cityPrefix)
var city: City,
#Embedded(prefix = statePrefix)
var state: State
) {
companion object {
const val cityPrefix = "city_"
const val statePrefix = "state_"
}
}
The query :-
/*
* Returns multiple columns renamed according to the prefix=? coded in the
* #Embedded annotation
*
*/
#Query("SELECT address.*, " +
"city.id AS " + AddressWithCityWithStateViaEmbeds.cityPrefix + "id," +
"city.name AS " + AddressWithCityWithStateViaEmbeds.cityPrefix + "name," +
"city.statefk AS " + AddressWithCityWithStateViaEmbeds.cityPrefix + "statefk," +
"state.id AS " + AddressWithCityWithStateViaEmbeds.statePrefix + "id," +
"state.name AS " + AddressWithCityWithStateViaEmbeds.statePrefix + "name " +
"FROM address JOIN city ON address.cityfk = city.id JOIN state ON city.statefk = state.id")
fun getAddressesWithCityAndStateViaEmbedPOJO(): List<AddressWithCityWithStateViaEmbeds>
4- POJO's with parent EMBED and child RELATE
The POJO's :-
data class CityWithState(
#Embedded
var city: City,
#Relation(
entity = State::class,
parentColumn = "statefk",
entityColumn = "id"
)
var state: State
)
and :-
data class AddressWithCityWithStateViaRelations(
#Embedded
var address: Address,
#Relation(
entity = City::class, /* NOTE NOT CityWithState which isn't an Entity */
parentColumn = "cityfk",
entityColumn = "id"
)
var cityWithState: CityWithState
)
and the query :-
#Transaction
#Query("SELECT * FROM address")
fun getAddressesWithCityAndStateViaRelations(): List<AddressWithCityWithStateViaRelations>
note the use of #Tranaction so the underlying queries, built by Room, are all done within a single database transaction.
Putting the above into use
The following code in an activity uses all 4 to output the same results :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val TAG: String = "DBINFO"
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
var state = State(1)
state.name = "State1"
val state1Id = dao.insert(state)
state.id = 2
state.name = "State2"
val state2Id = dao.insert(state)
var city = City(10)
city.name = "City1"
city.statefk = state1Id
val city1Id = dao.insert(city)
city.id = 11
city.name = "City2"
city.statefk = state2Id
val city2Id = dao.insert(city)
city.id = 12
city.name = "City3"
city.statefk = state1Id
val city3Id = dao.insert(city)
var address = Address(100)
address.name = "Address1"
address.cityfk = city1Id
dao.insert(address)
address.id = address.id + 1
address.name = "Address2"
address.cityfk = city2Id
dao.insert(address)
address.id = address.id + 1
address.name = "Address3"
address.cityfk = city3Id
for (s: String in dao.getAddressesAsStrings()) {
Log.d(TAG + "STRG", s)
}
for (awcws: AddressWithCityWithState in dao.getAddressesWithCityAndStateViaBasicPOJO()) {
Log.d(TAG + "BASICPOJO", "${awcws.address_name}, ${awcws.city_name}, ${awcws.state_name}")
}
for (awcwsve: AddressWithCityWithStateViaEmbeds in dao.getAddressesWithCityAndStateViaEmbedPOJO()) {
Log.d(TAG + "EMBEDS","${awcwsve.address.name}, ${awcwsve.city.name}, ${awcwsve.state.name}")
}
for(awcwsvr: AddressWithCityWithStateViaRelations in dao.getAddressesWithCityAndStateViaRelations()) {
Log.d(TAG + "MIXED","${awcwsvr.address.name}, ${awcwsvr.cityWithState.city.name}, ${awcwsvr.cityWithState.state.name}")
}
}
}
The output to the log being :-
2021-11-22 12:33:54.322 D/DBINFOSTRG: Address1,City1,State1
2021-11-22 12:33:54.322 D/DBINFOSTRG: Address2,City2,State2
2021-11-22 12:33:54.324 D/DBINFOBASICPOJO: Address1, City1, State1
2021-11-22 12:33:54.324 D/DBINFOBASICPOJO: Address2, City2, State2
2021-11-22 12:33:54.326 D/DBINFOEMBEDS: Address1, City1, State1
2021-11-22 12:33:54.326 D/DBINFOEMBEDS: Address2, City2, State2
2021-11-22 12:33:54.332 D/DBINFOMIXED: Address1, City1, State1
2021-11-22 12:33:54.332 D/DBINFOMIXED: Address2, City2, State2

Android Room belongsTo (one to one) relationship

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

Return type for Android Room joins

Let's say I want to do an INNER JOIN between two entities Foo and Bar:
#Query("SELECT * FROM Foo INNER JOIN Bar ON Foo.bar = Bar.id")
List<FooAndBar> findAllFooAndBar();
Is it possible to force a return type like this?
public class FooAndBar {
Foo foo;
Bar bar;
}
When I try to do that, I get this error:
error: Cannot figure out how to read this field from a cursor.
I've also tried aliasing the table names to match the field names, but that didn't work either.
If this isn't possible, how should I cleanly construct a compatible return type that includes all fields for both entities?
Dao
#Query("SELECT * FROM Foo")
List<FooAndBar> findAllFooAndBar();
Class FooAndBar
public class FooAndBar {
#Embedded
Foo foo;
#Relation(parentColumn = "Foo.bar_id", entityColumn = "Bar.id")
List<Bar> bar;
// If we are sure it returns only one entry
// Bar bar;
//Getter and setter...
}
This solution seems to work, but I'm not very proud of it.
What do you think about it?
Edit: Another solution
Dao, I prefer to explicitly select but "*" will do the job :)
Keep in mind that this solution only works when the fields of both entities are unique. See the comments for more information.
#Query("SELECT Foo.*, Bar.* FROM Foo INNER JOIN Bar ON Foo.bar = Bar.id")
List<FooAndBar> findAllFooAndBar();
Class FooAndBar
public class FooAndBar {
#Embedded
Foo foo;
#Embedded
Bar bar;
//Getter and setter...
}
edit: since Version 2.2.0-alpha01, room #Relation annotation can manage One-To-One relation
Another option is to just write a new POJO representing the resulting structure of your JOIN query (which even supports column renaming to avoid clashes):
#Dao
public interface FooBarDao {
#Query("SELECT foo.field1 AS unique1, bar.field1 AS unique2 "
+ "FROM Foo INNER JOIN Bar ON Foo.bar = Bar.id")
public List<FooBar> getFooBars();
static class FooBar {
public String unique1;
public String unique2;
}
}
See: room/accessing-data.html#query-multiple-tables
Try this way.
For example, I have M2M (many-to-many) relations between Product and Attribute. Many Products have many Attributes and I need to get all Attributes by Product.id with sorted records by PRODUCTS_ATTRIBUTES.DISPLAY_ORDERING.
|--------------| |--------------| |-----------------------|
| PRODUCT | | ATTRIBUTE | | PRODUCTS_ATTRIBUTES |
|--------------| |--------------| |-----------------------|
| _ID: Long | | _ID: Long | | _ID: Long |
| NAME: Text | | NAME: Text | | _PRODUCT_ID: Long |
|______________| |______________| | _ATTRIBUTE_ID: Long |
| DISPLAY_ORDERING: Int |
|_______________________|
So, models will be like below:
#Entity(
tableName = "PRODUCTS",
indices = [
Index(value = arrayOf("NAME"), unique = true)
]
)
class Product {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "_ID")
var _id: Long = 0
#ColumnInfo(name = "NAME")
#SerializedName(value = "NAME")
var name: String = String()
}
#Entity(
tableName = "ATTRIBUTES",
indices = [
Index(value = arrayOf("NAME"), unique = true)
]
)
class Attribute {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "_ID")
var _id: Long = 0
#ColumnInfo(name = "NAME")
#SerializedName(value = "NAME")
var name: String = String()
}
And the "join" table will be:
#Entity(
tableName = "PRODUCTS_ATTRIBUTES",
indices = [
Index(value = ["_PRODUCT_ID", "_ATTRIBUTE_ID"])
],
foreignKeys = [
ForeignKey(entity = Product::class, parentColumns = ["_ID"], childColumns = ["_PRODUCT_ID"]),
ForeignKey(entity = Attribute::class, parentColumns = ["_ID"], childColumns = ["_ATTRIBUTE_ID"])
]
)
class ProductAttribute {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "_ID")
var _id: Long = 0
#ColumnInfo(name = "_PRODUCT_ID")
var _productId: Long = 0
#ColumnInfo(name = "_ATTRIBUTE_ID")
var _attributeId: Long = 0
#ColumnInfo(name = "DISPLAY_ORDERING")
var displayOrdering: Int = 0
}
In, AttributeDAO, to get all attributes based on Product._ID, you can do something like below:
#Dao
interface AttributeDAO {
#Query("SELECT ATTRIBUTES.* FROM ATTRIBUTES INNER JOIN PRODUCTS_ATTRIBUTES ON PRODUCTS_ATTRIBUTES._ATTRIBUTE_ID = ATTRIBUTES._ID INNER JOIN PRODUCTS ON PRODUCTS._ID = PRODUCTS_ATTRIBUTES._PRODUCT_ID WHERE PRODUCTS._ID = :productId ORDER BY PRODUCTS_ATTRIBUTES.DISPLAY_ORDERING ASC")
fun getAttributesByProductId(productId: Long): LiveData<List<Attribute>>
}
If you have any questions, please tell me.
Is it possible to force a return type like this?
You can try #Embedded annotations on foo and bar. That will tell Room to try to take the columns from your query and pour them into foo and bar instances. I have only tried this with entities, but the docs indicate that it should work with POJOs as well.
However, this may not work well if your two tables have columns with the same name.
This is my Food Table
#Entity(tableName = "_food_table")
data class Food(#PrimaryKey(autoGenerate = false)
#ColumnInfo(name = "_food_id")
var id: Int = 0,
#ColumnInfo(name = "_name")
var name: String? = "")
This is my Cart table and model class (Food Cart)
#Entity(tableName = "_client_cart_table")
data class CartItem(
#PrimaryKey(autoGenerate = false)
#ColumnInfo(name = "_food_id")
var foodId: Int? = 0,
#Embedded(prefix = "_food")
var food: Food? = null,
#ColumnInfo(name = "_branch_id")
var branchId: Int = 0)
Note: Here we see _food_id column in two table. It will throw compile time error. From #Embedded doc, you have to use prefix to differentiate between them.
Inside dao
#Query("select * from _client_cart_table inner join _food_table on _client_cart_table._food_id = _food_table._food_id where _client_cart_table._branch_id = :branchId")
fun getCarts(branchId: Int) : LiveData<List<CartItem>>
This query will return data like this
CartItem(foodId=5, food=Food(id=5, name=Black Coffee), branchId=1)
I have done this in my project. So Give it a try. Happy coding

Categories

Resources