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
Related
I have three Models
#Entity(foreignKeys = [ForeignKey(entity = SelfHelpGroup::class, parentColumns = ["shgId"], childColumns = ["shgId"], onDelete = CASCADE), ForeignKey(entity = Member::class, parentColumns = ["memberId"], childColumns = ["memberId"], onDelete = CASCADE)])
data class Committee(
#PrimaryKey(autoGenerate = true)
#SerializedName("committeeId")
val committeeId: Int?= null,
#SerializedName("shgId")
val shgId: Int?,
#SerializedName("memberId")
val memberId: Int?,
#SerializedName("date")
val date: String?
)
#Entity(tableName = "Member", foreignKeys = [ForeignKey(entity = SelfHelpGroup::class, parentColumns = ["shgId"], childColumns = ["shgId"], onDelete = CASCADE), ForeignKey(entity = Role::class, parentColumns = ["roleId"], childColumns = ["roleId"], onDelete = CASCADE)])
data class Member(
#PrimaryKey(autoGenerate = true)
#SerializedName("memberId")
val memberId: Int ?= null,
#SerializedName("shgId")
val shgId: Int,
#SerializedName("name")
val name: String,
#SerializedName("address")
val address: String,
#SerializedName("phoneNumber")
val phoneNumber: String,
#SerializedName("emailId")
val emailId: String,
#SerializedName("roleId")
val roleId: Int?,
#SerializedName("password")
val password: String?
)
#Entity(foreignKeys = [
ForeignKey(entity = Committee::class, parentColumns = ["committeeId"], childColumns = ["committeeId"], onDelete = CASCADE),
ForeignKey(entity = Member::class, parentColumns = ["memberId"], childColumns = ["memberId"], onDelete = CASCADE),
])
data class Attendance(
#PrimaryKey(autoGenerate = true)
#SerializedName("attendanceId")
val attendanceId: Int?= null,
#SerializedName("committeeId")
val committeeId: Int,
#SerializedName("memberId")
val memberId: Int,
/*#SerializedName("status")
val status: AttendanceStatus,*/
#SerializedName("isPresent")
var isPresent: Boolean = false,
#SerializedName("leaveApplied")
var leaveApplied: Boolean = false
)
Relation between 3 models :
Any member can host a committee.
The hosted memberId is saved in the table Member.
Other members can join the committee.
To track the attendance of these members, we are using the Table Attendance.
So I need help queriying the data in such a way that the result structure would look like below
data class CommitteeDetails (
val committeeId: Int,
val member: Member,
val attendances: List<Attendance>,
val dateTime: String
)
Since there are more than many committees, I need to query to get Listof CommitteeDetails
val committees = List<CommitteeDetails>()
The easiest way would be to use:-
data class CommitteeDetails (
//val committeeId: Int, /* part of the Committee so get the Committee in it's entireity */
#Embedded
val committee: Committee,
#Relation(entity = Member::class, parentColumn = "memberId", entityColumn = "memberId")
val member: Member,
#Relation(entity = Attendance::class, parentColumn = "committeeId", entityColumn = "committeeId")
val attendances: List<Attendance>
//val dateTime: String
)
This does then retrieve a little more information but the Query is very simple as you just query the committee table.
e.g. to get ALL Committees with the Member and the attendances then you can use
#Transaction
#Query("SELECT * FROM committee")
#Tranaction is not mandatory but if not used will result in a warning e.g.
warning: The return value includes a POJO with a `#Relation`. It is usually desired to annotate this method with `#Transaction` to avoid possibility of inconsistent results between the POJO and its relations. See https://developer.android.com/reference/androidx/room/Transaction.html for details.
This is because #Relation results in Room effectively running subqueries to obtain the related items. Note that using #Relation will return ALL the related items for the #Embedded.
IF you wanted to get exactly what you have asked for then it's a little more convoluted as you would then have to not use #Embedded for the Committee and thus you could then not use #Relation.
In theory you would SELECT FROM the committee table, JOIN the member table and also JOIN the attendance table. The issue is that the result is the cartesian map so for every attendance per committee you would get a result that contained the committee columns (id and date) the member columns (all to build the full Member class) and all the columns from the attendance. However, the issue, is then building the CommitteeDetails.
However, you can mimic how room works and just get the desired committee column along with the member columns and then invoke a subquery to obtain the related attendances (potentially filtering them).
So say you have (wanting a List of these as the end result):-
data class CommitteeDetailsExact (
val committeeId: Int,
val member: Member,
val attendances: List<Attendance>,
val dateTime: String
)
The to facilitate the initial extraction of the committee and members columns you could have another POJO such as:-
data class CommitteeIdAndDateAsDateTimeWithMember(
val committeeId: Int,
#Embedded
val member: Member,
val dateTime: String
)
To facilitate extracting the data you could have functions such as:-
#Query("SELECT committee.committeeId, committee.date AS dateTime, member.* FROM committee JOIN member ON committee.memberId = member.memberId")
fun getCommitteeExactLessAttendances(): List<CommitteeIdAndDateAsDateTimeWithMember>
#Query("SELECT * FROM attendance WHERE committeeId=:committeeId")
fun getCommitteeAttendances(committeeId: Int): List<Attendance>
To obtain the end result then the above functions need to be used together, so you could have a function such as:-
#Transaction
#Query("")
fun getExactCommitteeDetails(): List<CommitteeDetailsExact> {
var rv = arrayListOf<CommitteeDetailsExact>()
for (ciadadwm in getExactCommitteeDetails()) {
rv.add(CommitteeDetailsExact(ciadadwm.committeeId,ciadadwm.member,getCommitteeAttendances(ciadadwm.committeeId),ciadadwm.dateTime))
}
return rv.toList()
}
This will:-
return the desired list of committee details (albeit List<CommitteeDetailsExact> to suite the two answers given)
run as a single transaction (the #Query(") enables Room to apply the #Transaction)
Obtains the list of committee and member columns an then
Loops through the list extract the respective list of attendances
in short it, in this case, is very much similar to the first answer other than limiting the columns extracted from the committee table.
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
So I am build an app where I want to store information coming from various source into a single database. The database is as below:
#Entity(tableName = "store")
data class StoreEntity (
#PrimaryKey()
#ColumnInfo(name = "")
var current: String,
#ColumnInfo(name = "ingredientslist")
var productsIngredientList: MutableList<IngredientsEntity?>,
#ColumnInfo(name = "reviewslist")
var productsReviewsList: MutableList<ReviewsEntity?>,
#ColumnInfo(name = "listofproductsId")
var listOfId: MutableList<String>
)
and the other entities are:
#Entity(tableName = "ingredients")
data class IngredientsEntity(
#PrimaryKey()
#NonNull var id: String,
#ColumnInfo(name = "ingredient")
var ingredient: Ingredient? = null,
)
#Entity(tableName = "reviews")
data class IngredientsEntity(
#PrimaryKey()
#NonNull var id: String,
#ColumnInfo(name = "review")
var review: Review? = null,
)
I have defined the Doa as below:
#Query("UPDATE store SELECT ingredientslist SET ingredient=:shadow WHERE id = :id ")
suspend fun updateIngredients(id: String?, ingredient: Ingredient)
#Query("SELECT ingredient from ingredientslist WHERE id = :id ")
suspend fun getIngredients(id: String): Flow<Ingredient?>
suspend fun getIngredientsDistinctUntilChanged(id: String) = getIngredients(id).distinctUntilChanged()
reviews and ingredients are both coming from different sources but I would like a database where I can store :
current product displayed -> String (only one)
list of products -> list of String
list of Ingredients (List of id + ingredients)
list of Reviews (List of id + reviews)
the goal would be to be able to add/retrieve ingredients using id and same for reviews.
I am not sure if I have to access using the Doa the store database, then select ingrediendslist, then look for ingredients link to an id. or if I can just access directky ingrediendslist because store is automatically linking ingredientslist
Also is there a way to have a single PrimaryKey, which could be a single name ? the Store database will only have one single entry. See this like you can have only one store.
It's look likes to me a database into a database.
Any idea how to make it works ? I tried several Room sql command but I am not sure that it's the right way.
Maybe I need to split it into different Dbs. One for ingredients, one for reviews and one for current product id and list of products ids.
I was trying to do it in one single database to avoid having multiple DBs for only 15 products top.
Any idea or advices ?
Thanks
The database is as below:
That would be in theory, there are various issues that would result in the compilation failing. Such as trying to give a column no name as per :-
#Entity(tableName = "store")
data class StoreEntity (
#PrimaryKey()
#ColumnInfo(name = "")
var current: String,
Furthermore to store a list of Objects, although possible with the use of TypeConverters restricts or complicates matters with regard to best use of the database. Doing so also de-normalises the database and adds bloat as an object if referred to multiple times (such as an ingredient which may in theory be common to a number of stores).
Complexity arises through a value of any such object being stored with all the other values and thus would require complex SQL very likely including CASE THEN ELSE END constructs within and possibly Common Table Expressions.
As you are in the development stage I would suggest consider using the power of relationships basically have an extra 2 tables one to map/reference/associate a Store with it's ingredients, another to map a Store with it's Reviews.
Here's a working example (without products but the same technique applies for products)
So first the StoreEntity :-
#Entity(tableName = "store")
data class StoreEntity (
#PrimaryKey()
#ColumnInfo(name = "store_identifier")
var current: String
/* lists are mapped so not needed here (see StoreWithMappedReviewsAndWithMappedIngredients)
#ColumnInfo(name = "ingredientslist")
var productsIngredientList: MutableList<IngredientsEntity?>,
#ColumnInfo(name = "reviewslist")
var productsReviewsList: MutableList<ReviewsEntity?>,
#ColumnInfo(name = "listofproductsId")
var listOfId: MutableList<String>
*/
)
The Review class (made up as not included) :-
data class Review(
var reviewTitle: String,
var reviewer: String
/* etc */
)
And like your code the respective ReviewsEnntity (that Embeds (copies the member variables) the underlying Review) :-
#Entity(tableName = "reviews")
data class /*IngredientsEntity????*/ ReviewsEntity(
#PrimaryKey()
#NonNull var reviewId: String,
/* see comments for Ingredient class
#ColumnInfo(name = "review")
var review: /*Reviews? ????*/ Review = null,
*/
#Embedded
var review: Review
)
Similar for Ingredient (made up again) and IngredientsEntity :-
data class Ingredient(
var ingredientName: String,
var ingredientDescription: String
/* other member variables as required */
)
and :-
#Entity(tableName = "ingredients")
data class IngredientsEntity(
#PrimaryKey
#NonNull var ingredientId: String,
#Embedded
var ingredient: Ingredient
/* using below would require a TypeConverter the above embeds the ingredient so a column as per the ingredient class */
/*var ingredient: Ingredient? = null <<<<< null ???? why have an ingredient row with no ingredient? */
)
Now the two (3rd for products) mapping/associate/reference .... tables.
First the table that maps Stores and Reviews with the potential for many stores to have many Reviews and for Reviews to map to many Stores.
StoreReviewMappingEntity
#Entity(
tableName = "store_review_map",
primaryKeys = ["storeReviewMap_storeId","storeReviewMap_reviewId"]
/* Optional Foreign Key Constraints to enforce Referential Integrity */
)
data class StoreReviewMappingEntity(
var storeReviewMap_storeId: String,
#ColumnInfo(index = true)
var storeReviewMap_reviewId: String
)
and StoreIngredientMappingEntity for Ingredients :-
#Entity(
tableName = "store_ingredient_map",
primaryKeys = ["storeIngredientMap_storeId","storeIngredientMap_ingredientId"]
/* Optional Foreign Key Constraints to enforce Referential Integrity */
)
data class StoreIngredientMappingEntity(
var storeIngredientMap_storeId: String,
#ColumnInfo(index = true) /* index on the ingredient id so getting the ingredient is more efficient */
var storeIngredientMap_ingredientId: String
)
So instead of the 3 tables 5 tables (entities)
Now a POJO (i.e. not an Entity) StoreWithMappedReviewsAndWithMappedIngredients that allows you to get a Store with all of it's Reviews and all of it's ingredients via the mapping tables:-
data class StoreWithMappedReviewsAndWithMappedIngredients(
#Embedded
var storeEntity: StoreEntity,
#Relation(
entity = ReviewsEntity::class,
parentColumn = "store_identifier",
entityColumn = "reviewId",
associateBy = Junction(
value = StoreReviewMappingEntity::class /* the mappping table class/entity */,
parentColumn = "storeReviewMap_storeId",
entityColumn = "storeReviewMap_reviewId"
)
)
var reviewList: List<ReviewsEntity>,
#Relation(
entity = IngredientsEntity::class,
parentColumn = "store_identifier",
entityColumn = "ingredientId",
associateBy = Junction(
value = StoreIngredientMappingEntity::class,
parentColumn = "storeIngredientMap_storeId",
entityColumn = "storeIngredientMap_ingredientId"
)
)
var ingredientList: List<IngredientsEntity>
)
Now the #Dao annotated interface (one for brevity/convenience) AllDao :-
#Dao
interface AllDao {
/*
#Query("UPDATE store SELECT ingredientslist SET ingredient=:shadow WHERE id = :id ")
suspend fun updateIngredients(id: String?, ingredient: Ingredient)
#Query("SELECT ingredient from ingredientslist WHERE id = :id ")
suspend fun getIngredients(id: String): Flow<Ingredient?>
suspend fun getIngredientsDistinctUntilChanged(id: String) = getIngredients(id).distinctUntilChanged()
*/
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(ingredientsEntity: IngredientsEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(reviewsEntity: ReviewsEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(storeEntity: StoreEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(storeReviewMappingEntity: StoreReviewMappingEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(storeIngredientMappingEntity: StoreIngredientMappingEntity): Long
/* Allows removal of Review/Ingredient respectively (if only one store then no need for storeId but just-in-case) */
#Query("DELETE FROM store_review_map WHERE storeReviewMap_ReviewId=:reviewId AND storeReviewMap_StoreId=:storeId")
fun deleteReviewFromStore(storeId: String, reviewId: String): Int
#Query("DELETE FROM store_ingredient_map WHERE storeIngredientMap_IngredientId=:ingredientId AND storeIngredientMap_StoreId=:storeId")
fun deleteIngredientFromStore(storeId: String, ingredientId: String): Int
#Transaction
#Query("SELECT * FROM store")
fun getAllStoresWithReviewsAndWithIngredients(): List<StoreWithMappedReviewsAndWithMappedIngredients>
}
Note for the example .allowMainThreadQueries has been used so suspend not used just add as appropriate.
Nearly there here's an #Database annotated class :-
#Database(entities = [
StoreEntity::class,
ReviewsEntity::class,
IngredientsEntity::class,
StoreReviewMappingEntity::class,
StoreIngredientMappingEntity::class
],
version = 1,
exportSchema = false /* consider true see https://developer.android.com/training/data-storage/room/migrating-db-versions */
)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
#Volatile
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
context,
TheDatabase::class.java,
"the_database.db"
)
.allowMainThreadQueries() /* for convenience/brevity of the example */
.build()
}
return instance as TheDatabase
}
}
}
Finally putting it al together in an Activity which inserts 2 Stores, 4 reviwa, 4 Ingredients and maps reviews and ingredients to only the first Store and finally extracts all of the Stores with the Reviews and Ingredients and writes the result to the log. :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
val store1Id = "S001"
val store2Id = "S002"
val store1 = dao.insert(StoreEntity(store1Id))
val store2 = dao.insert(StoreEntity(store2Id))
val mercuryId = "I001"
val silverId = "I002"
val copperid = "I003"
val zincId = "I004"
val ing01= dao.insert( IngredientsEntity(ingredientId = mercuryId, Ingredient("Mercury","Quicksilver- Hazardous")))
val ing02 = dao.insert(IngredientsEntity(silverId, Ingredient("Silver","Au - Metal - Safe blah ...")))
val ing03 = dao.insert(IngredientsEntity( ingredient = Ingredient("Copper","Cu - Metal - Safe"),ingredientId = copperid))
val ing04 = dao.insert(IngredientsEntity(ingredientId = zincId,ingredient = Ingredient(ingredientDescription = "Zn - Metal - Safe", ingredientName = "Zinc")))
/* note ing?? values will be either 1 or greater (the rowid) or -1 if the row was not inserted (e.g. duplicate conflict ignored)*/
val r1Id = "R001"
val r2Id = "R002"
val r3Id = "R003"
val r4Id = "R004"
val r1 = dao.insert(ReviewsEntity(r1Id, Review("Review 1 - etc","Reviewer1")))
val r2 = dao.insert(ReviewsEntity(r2Id,Review("Review 2 - etc","Reviewer9")))
val r3 = dao.insert(ReviewsEntity(r3Id,Review("Review 3 - etc","Reviewer1")))
val r4 = dao.insert(ReviewsEntity(r4Id,Review("Review 4 - etc","Reviewer8")))
dao.insert(StoreReviewMappingEntity(store1Id,r2Id))
dao.insert(StoreReviewMappingEntity(store1Id,r3Id))
dao.insert(StoreReviewMappingEntity(store1Id,r1Id))
dao.insert(StoreReviewMappingEntity(store1Id,r4Id))
dao.insert(StoreIngredientMappingEntity(store1Id,zincId))
dao.insert(StoreIngredientMappingEntity(store1Id,mercuryId))
dao.insert(StoreIngredientMappingEntity(store1Id,copperid))
dao.insert(StoreIngredientMappingEntity(store1Id,silverId))
dao.insert(StoreIngredientMappingEntity(store1Id,zincId)) //<<<< due to onconflict ignore will not be inserted as duplicate
val sb: java.lang.StringBuilder = java.lang.StringBuilder().append("Extracting All Stores with Reviews and Ingredients:-")
for (swmrawmi: StoreWithMappedReviewsAndWithMappedIngredients in dao.getAllStoresWithReviewsAndWithIngredients()) {
sb.append("\nCurrentStore is ${swmrawmi.storeEntity.current} it has ${swmrawmi.reviewList.size} Reviews and ${swmrawmi.ingredientList.size} ingredients")
sb.append("\n\tThe Reviews Are:-")
for(r: ReviewsEntity in swmrawmi.reviewList) {
sb.append("\n\t\tID is ${r.reviewId}, Title is ${r.review.reviewTitle} etc")
}
sb.append("\n\tThe ingredients Are :-")
for(i: IngredientsEntity in swmrawmi.ingredientList) {
sb.append("\n\t\tID is ${i.ingredientId}, Name is ${i.ingredient.ingredientName} Description is ${i.ingredient.ingredientDescription}")
}
}
Log.d("RESULTINFO",sb.toString())
}
}
and the result from the log when run :-
D/RESULTINFO: Extracting All Stores with Reviews and Ingredients:-
CurrentStore is S001 it has 4 Reviews and 4 ingredients
The Reviews Are:-
ID is R001, Title is Review 1 - etc etc
ID is R002, Title is Review 2 - etc etc
ID is R003, Title is Review 3 - etc etc
ID is R004, Title is Review 4 - etc etc
The ingredients Are :-
ID is I001, Name is Mercury Description is Quicksilver- Hazardous
ID is I002, Name is Silver Description is Au - Metal - Safe blah ...
ID is I003, Name is Copper Description is Cu - Metal - Safe
ID is I004, Name is Zinc Description is Zn - Metal - Safe
CurrentStore is S002 it has 0 Reviews and 0 ingredients
The Reviews Are:-
The ingredients Are :-
I am trying to save a list of custom data types in room database, actually, I want one of the column should contain list of all transactions show that I don't have to two tables.
account.kt
#Entity(tableName = "account_table")
data class account(
#ColumnInfo(name="name")
val name:String,
#ColumnInfo(name="transaction")
val transactions:List<transaction>
){
#PrimaryKey(autoGenerate = true)#ColumnInfo(name="account_id")
val accountId:Int=0
}
transaction.kt
data class transaction(
val amount:Float,
val description:String,
val date:String
)
Now when I am running the code I get an error
error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
private final java.util.List<com.example.ledger.Database.transaction> transactions = null;
^D:\flutterProjects\Ledger\app\build\tmp\kapt3\stubs\debug\com\example\ledger\Database\account.java:10: error: Cannot find setter for field.
private final int accountId = 0;
D:\flutterProjects\Ledger\app\build\tmp\kapt3\stubs\debug\com\example\ledger\Database\accountsDao.java:19: warning: The query returns some columns [account_id, name, transaction] which are not used by com.example.ledger.Database.transaction. You can use #ColumnInfo annotation on the fields to specify the mapping. com.example.ledger.Database.transaction has some fields [amount, description, date] which are not returned by the query. If they are not supposed to be read from the result, you can mark them with #Ignore annotation. You can suppress this warning by annotating the method with #SuppressWarnings(RoomWarnings.CURSOR_MISMATCH). Columns returned by the query: account_id, name, transaction. Fields in com.example.ledger.Database.transaction: amount, description, date.
public abstract java.util.List<com.example.ledger.Database.transaction> getTransaction(int id);
^D:\flutterProjects\Ledger\app\build\tmp\kapt3\stubs\debug\com\example\ledger\Database\accountsDao.java:19: error: The columns returned by the query does not have the fields [amount,description,date] in com.example.ledger.Database.transaction even though they are annotated as non-null or primitive. Columns returned by the query: [account_id,name,transaction]
public abstract java.util.List<com.example.ledger.Database.transaction> getTransaction(int id);
^[WARN] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: androidx.room.RoomProcessor (DYNAMIC).
EDIT
accountDao.kt
#Dao
interface accountsDao {
#Insert
fun Insert(account: account)
#Delete
fun delete(account: account)
#Query("SELECT * FROM account_table where account_id = :id ")
fun getTransaction(id:Int):List<transaction>
}
accountDatabase
#Database(entities = [account::class], version = 1, exportSchema = false)
abstract class accountsDatabase : RoomDatabase() {
abstract val accountdao: accountsDao
companion object {
#Volatile
private var INSTANCE: accountsDatabase? = null
fun getInstance(context: Context): accountsDatabase? {
synchronized(this) {
// Copy the current value of INSTANCE to a local variable so Kotlin can smart cast.
// Smart cast is only available to local variables.
var instance = INSTANCE
// If instance is `null` make a new database instance.
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
accountsDatabase::class.java,
"saccount_history_database"
)
.fallbackToDestructiveMigration()
.build()
// Assign INSTANCE to the newly created database.
INSTANCE = instance
}
// Return instance; smart cast to be non-null.
return instance
}
}
}
}
I feel there would some code of type convertor in Dao file because I am accessing List<transaction>
For example, lists, models, model list etc. When you want to store some special type objects, such as in the database, you can use Type Converters. Converts your custom object type to a type known for database types. This is the most useful feature of the Room persistent library.
account.kt:
#Entity(tableName = "account_table")
data class account(
#PrimaryKey(autoGenerate = true)#ColumnInfo(name="account_id")
val accountId:Int=0,
#ColumnInfo(name="name")
val name:String,
#TypeConverters(Converters::class)
#ColumnInfo(name="transaction")
val transactions:List<transaction>
)
Converters.kt:
class Converters {
#TypeConverter
fun fromTransactionList(transaction: List<transaction?>?): String? {
if (transaction == null) {
return null
}
val gson = Gson()
val type: Type = object : TypeToken<List<transaction?>?>() {}.type
return gson.toJson(transaction, type)
}
#TypeConverter
fun toTransactionList(transactionString: String?): List<transaction>? {
if (transactionString == null) {
return null
}
val gson = Gson()
val type =
object : TypeToken<List<transaction?>?>() {}.type
return gson.fromJson<List<transaction>>(transactionString, type)
}
}
You can add like this:
#Database(entities = [account::class], version = 1, exportSchema = false)
#TypeConverters(Converters::class)
abstract class accountsDatabase : RoomDatabase()
And interface accountsDao:
#Query("SELECT * FROM account_table where account_id = :id ")
fun getTransaction(id:Int):List<account>
using:
GlobalScope.launch {
val db = accountsDatabase.getInstance(applicationContext)
val accounts = db.accountDao().getTransaction(your_id)
val transactions = accounts[position].transactions
transactions?.forEach {
Log.d("Transaction Description" ,it.description)
}
}
Long story short: you should add a new table. Room is specifically built for you to use a separate table here, you are sacrificing performance and readability to do it this way. Instead, you should define:
#Entity(
ForeignKey(
parentColumns = ["account_id"],
entityColumns = ["account_id"],
entity = Account::class,
onDelete = ForeignKey.CASCADE, // if you delete the account, transactions are
//deleted as well
// you can add an onUpdate cascade, but you shouldn't update account numbers at
//all in my opinion
) // you'll also want add indexes on (accountId, transactionId), (accountId, date) as well
)
data class Transaction(
#PrimaryKey(autoGenerate = true)
val transactionId : Int,
#ColumnInfo(name="account_id")
val accountId : Int,
val amount:Float,
val description:String,
val date:String
)
Now under account you simply add:
#Entity(tableName = "account_table")
data class account(
#ColumnInfo(name="name")
val name:String,
#Relation(
parentColumn = "account_id",
entity_column="account_id",
entity=Transaction:class
)
val transactions:List<Transaction>
){
#PrimaryKey(autoGenerate = true)#ColumnInfo(name="account_id")
val accountId:Int=0
}
I am working with room persistence library in android, i would appreciate if someone can help me in using foreign key, how to get data by using foreign key.
Just to summarize the above posts for future readers:
The foreign key syntax in Kotlin is
#Entity(foreignKeys = arrayOf(ForeignKey(entity = ParentClass::class,
parentColumns = arrayOf("parentClassColumn"),
childColumns = arrayOf("childClassColumn"),
onDelete = ForeignKey.CASCADE)))
The foreign key syntax in Java is:
#Entity(foreignKeys = {#ForeignKey(entity = ParentClass.class,
parentColumns = "parentClassColumn",
childColumns = "childClassColumn",
onDelete = ForeignKey.CASCADE)
})
Note: foreignKeys is an array, so in Java enclose #ForeignKey elements in { and }
You can refer to the official documentation for more information.
https://developer.android.com/reference/androidx/room/ForeignKey
Here how you can define and access a One-to-many (Foreign Key) relationship in Android Jetpack Room. Here the Entities are Artist and Album and the foreign key is Album.artist
#Entity
data class Artist(
#PrimaryKey
val id: String,
val name: String
)
#Entity(
foreignKeys = [ForeignKey(
entity = Artist::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("artist"),
onDelete = ForeignKey.CASCADE
)]
)
data class Album(
#PrimaryKey
val albumId: String,
val name: String,
#ColumnInfo(index = true)
val artist: String
)
then the embedded object (read official Android documentation: Define relationships between objects)
data class ArtistAndAlbums(
#Embedded
val artist: Artist,
#Relation(
parentColumn = "id",
entityColumn = "artist"
)
val albums: List<Album>
)
And finally the DAO
#Dao
interface Library {
#Insert
suspend fun save(artist: Artist)
#Insert
suspend fun save(vararg album: Album)
#Transaction
#Query("SELECT * FROM artist")
suspend fun getAll(): List<ArtistAndAlbums>
#Transaction
#Query("SELECT * FROM artist WHERE id = :id")
suspend fun getByArtistId(id: String): ArtistAndAlbums
}
trying this out, (all these operations happens in a Coroutine)
// creating objects
val artist = Artist(id="hillsongunited", name="Hillsong United" )
val artist2 = Artist(id="planetshakers", name="Planet Shakers" )
val album1 = Album(albumId = "empires", name = "Empires", artist = artist.id)
val album2 = Album(albumId = "wonder", name = "Wonder", artist = artist.id)
val album3 = Album(albumId = "people", name = "People", artist = artist.id)
val album4 = Album(albumId = "rain", name = "Rain", artist = artist2.id)
val album5 = Album(albumId = "itschristmas", name = "Its Christmas", artist = artist2.id)
val album6 = Album(albumId = "overitall", name = "Over It All", artist = artist2.id)
// saving to database
SongDatabase.invoke(applicationContext).library().save(artist)
SongDatabase.invoke(applicationContext).library().save(artist2)
SongDatabase.invoke(applicationContext).library().save(album1, album2, album3, album4, album5, album6)
Logging out all Artists
val all = SongDatabase.invoke(applicationContext).library().getAll()
Log.d("debug", "All Artists $all ")
D/debug: All Artists [ArtistAndAlbums(artist=Artist(id=hillsongunited, name=Hillsong United), albums=[Album(albumId=empires, name=Empires, artist=hillsongunited), Album(albumId=wonder, name=Wonder, artist=hillsongunited), Album(albumId=people, name=People, artist=hillsongunited)]), ArtistAndAlbums(artist=Artist(id=planetshakers, name=Planet Shakers), albums=[Album(albumId=rain, name=Rain, artist=planetshakers), Album(albumId=itschristmas, name=Its Christmas, artist=planetshakers), Album(albumId=overitall, name=Over It All, artist=planetshakers)])]
Logging out albums by a specific artist,
val hillsongAlbums = SongDatabase.invoke(applicationContext).library().getByArtistId(artist.id)
Log.d("debug", "Albums by artist ID: $hillsongAlbums ")
D/debug: Albums by artist ID: ArtistAndAlbums(artist=Artist(id=hillsongunited, name=Hillsong United), albums=[Album(albumId=empires, name=Empires, artist=hillsongunited), Album(albumId=wonder, name=Wonder, artist=hillsongunited), Album(albumId=people, name=People, artist=hillsongunited)])
#ForeignKey annotations are not used to define relations when getting data but to define relations when modifying data. To get relational data from a Room databse, Google recommends the #Relation along with the #Embedded annotation.
You can check out my answer here for more explanation if you're interested.