Android Room apply constraint to Relation - android

I'm aware Room lets us establish 1-N relations with the #Relation keyword. Although I'd like to know if it's possible to apply conditions to this relationship.
Let's say that I have the following POJO
class UserAndPets {
#Embedded
lateinit var user: User
#Relation(entityColumn = "ownerId", parentColumn = "userId")
lateinit var pets: List<Pets>
}
interface UserDao {
#Query("SELECT * FROM users WHERE userId = :userId LIMIT 1")
fun getUserAndPetsForUserId(userId: String): UserAndPets?
}
The above method lets me query a User and all his pets. Although is there a way for me to query the User and his last 10 pets for example? Or a User and all his Pets that would be a certain type?
Thanks

If the Id field in Pets is auto-incremented then this query should give you the result you are looking for:
#Query("SELECT * FROM pets WHERE ownerId = :userId ORDER BY Id DESC LIMIT 10")
By the way, the "LIMIT 1" in your Users query is unneccessary, the query would always return a single user.

Related

Different accounts with different content, managing user preferences

On the bottom I added a picture of my current app structure and the current code for included data classes / entities.
At the moment in my app the user inserts the url and the code in the Login Fragment, clicking on save button the request to get the token starts. When successful the token is passed to the other requests to fetch the categories data. The different categories I get from the response are then showed in a recyclerview. By clicking on a category the user comes to the movies / seriers by genre Fragment, there I have another recyclerview with the list of movies or series.
When the token-request is successful the url & code are send also to a data class (entity) called AccountData, additional there is a unique String, put together of the url and the code, which works as Primary Key.
The AccountData is shown in a recyclerview on the Account Managment Fragment, which is the start screen of the app.
Now I want to give the user the option to select for each account, the categories he wants to have shown. Having the possibility to modify he's preferences everytime he wants.
For example:
AccountA has 10 movie categories, the user wants have shown only 5 of them.
AccountB has 15 movie categories, the user wants have shown only 6 of them.
My idea is to create a new Fragment, MovieCategorySelectFragment or so, where the user can click the categories he wants, passing the selected categories to the Movies Categories Fragment, like a Favorite-List. For implementing this I think about Room.
So I made the MovieCategory data class an Entity, using "Id" as Primary Key, and then, considering that it is a one to many relationship (I hope I am right with this), I added the Primary Key from the AccountData Entity to the MovieCategory Entity.
I made the String nullable -> val accountData: String?, that I don't get the NullpointerException error.
But now I'm stuck, would it be better to create a new data class / entity, calling it f.e. SelectedMovieCategory, and passing to it the selected item/category (from the MovieCategorySelectFragment, which is not part of a database) to it and using the room database then do display the selectet categories in the Movies Categories Fragment. Or should I make the request for the categories and save them immediately in the room-database and handling then the process of selecting?
And finally, on both methods, how can I pass the primary key from AccountData to MovieCategory? Otherwise there is no relationship between them? Do I have to create a function in the Dao to handle this?
At the end in the Account Management Fragment the user should be able to click on the account he wants to load, having loaded for each account only the categories he selected before. With the ability to change his preferences going into the MovieCategorySelectFragment and add or remove some categories from his "favorite-list".
Hopefully someone can help me to find the best and easiest way to handle this.
These are the data classes:
data class MovieCategoryResponse(
val js: List<MovieCategory>
)
#Entity
#Parcelize
data class MovieCategory(
#PrimaryKey(autoGenerate = false)
val id: String,
val number: Int,
val title: String,
val accountData: String?
) : Parcelable
#Entity
data class AccountData(
val url: String,
val code: String,
#PrimaryKey(autoGenerate = false)
val totalAccountData: String
)
It appears that you want accounts, moviecategories, seriescategories and a means of associating/relating accounts with a number of moviecategories and perhaps seriescategories and to be able to restrict the number of listed moviecategories according to an accounts preference which can change.
The solution would be a many-many relationship between account and moviecategories (and perhaps seriescategories).
However, before moving on you say:-
So I made the MovieCategory data class an Entity, using "Id" as Primary Key, and then, considering that it is a one to many relationship (I hope I am right with this), I added the Primary Key from the AccountData Entity to the MovieCategory Entity.
Regarding your hope. I believe that you are wrong. There are 3 types of relationships:-
1-1 where each row in a table would have a means of uniquely identifying the row, typically via a single column, in other table (if a separate table is apt (a single table may well suffice)). 1-1 relationships are not typically catered for by using tables.
1-many where a row in the parent table (account) could have many children (moviecategory) BUT the moviecategory could only have the 1 parent. In such a case a column in the moviecategory would contain a value that uniquely identifies the parent.
many-many an expanded 1-M that allows each side to relate to any number of the other side. So an account can relate to many moviecategories to which other accounts can relate to. The typical solution is to have an intermediate table that has 2 core columns. One that stores the value that uniquely identifies one side of the relationship and the other that stores the value that uniquely identifies the other side. Typically the 2 columns would for the primary key.
such an intermediate table has numerous terms to describe such a table, such as associative table, mapping table, reference table ....
Note how id has been highlighted. Simply creating a column called id in a table (Entity) does not make a relationship, it only supports the potential of a relationship being made.
You problem would appear to tick the boxes for a many-many relationship and thus the extra table (2 if account-secriescategories).
This table would have a column for the the value that uniquely identifies the accountData row (totalAccountData).
as the totalAccountData is the primary key (i.e. it is annotated with #PrimaryKey) and that a PrimaryKey is implicitly unique
The table would have a second column for the movieCategory's id column.
So you could start with
#Entity
data class AccountMovieMap(
val accountDataMap: String,
val movieCategoryMap: String
)
However, there is no PrimaryKey which room requires BUT the #PrimaryKey annotation only applies to a single column. If either were used then due to the implicit uniqueness the relationship would be restricted to 1-many. A composite (multiple columns/values) Primary Key is required that makes the uniqueness according to the combined values. To specify a composite PrimaryKey in Room the primaryKeys parameter of the #Entity annotation is used.
So AccountMovieMap becomes :-
#Entity(
primaryKeys = ["accountDataMap","movieCategoryMap"]
)
data class AccountMovieMap(
val accountDataMap: String,
val movieCategoryMap: String
)
As it stands there is a potential issue with the above as it is possible to insert data into either or both columns that is not a value in the respective table. That is the integrity of the relationship, in such a situation, does not exist.
SQLite and therefore Room (as with many relational database) caters for enforcing Referential Integrity. SQLite does this via ForeignKey clauses. Room uses the foreignKeys parameter of the #Entity annotation to provide a list of ForeignKeys.
In addition to enforcing referential integrity SQlite has 2 clauses ON DELETE and ON UPDATE that help to maintain referential integrity (depending upon the specified action, the most useful being CASCADE which allows changes that would break referential integrity by applying changes to the parent to the children).
Room will also warn if an index does not exist where it believe one should e.g. warning: movieCategoryMap column references a foreign key but it is not part of an index. This may trigger full table scans whenever parent table is modified so you are highly advised to create an index that covers this column. As such, the #ColumnInfo annotation can be used to add an index on the movieCategoryMap column.
So AccountMovieMap could be the fuller:-
#Entity(
primaryKeys = ["accountDataMap","movieCategoryMap"]
, foreignKeys = [
ForeignKey(
entity = AccountData::class,
parentColumns = ["totalAccountData"],
childColumns = ["accountDataMap"],
/* Optional but helps to maintain Referential Integrity */
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = MovieCategory::class,
parentColumns = ["id"],
childColumns = ["movieCategoryMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class AccountMovieMap(
val accountDataMap: String,
#ColumnInfo(index = true)
val movieCategoryMap: String
)
To add (insert) rows you could then have/use (in an #Dao annotated class):-
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(accountMovieMap: AccountMovieMap)
noting that to avoid referential integrity conflicts that the accountData referenced/mapped and MovieCategory referenced/mapped need to exist.
As you would want to extract an AccountData's MovieCategories you need a POJO that has the AccountData with a List of MovieCategory.
This could be:-
data class AccountWithMovieCategoryList(
#Embedded
val accountData: AccountData,
#Relation(
entity = MovieCategory::class,
parentColumn = "totalAccountData", /* The column referenced in the #Embedded */
entityColumn = "id", /* The column referenced in the #Relation (MovieCategory) */
/* The mapping table */
associateBy = (
Junction(
value = AccountMovieMap::class, /* The #Entity annotated class for the mapping table */
parentColumn = "accountDataMap", /* the column in the mapping table that references the #Embedded */
entityColumn = "movieCategoryMap" /* the column in the mapping table that references the #Relation */
)
)
)
val movieCategoryList: List<MovieCategory>
)
The following could be the function in an #Dao annotated interface that retrieves an AccountWithMovieCategoryList for a given account:-
#Transaction
#Query("SELECT * FROM accountData WHERE totalAccountData=:totalAccountData")
fun getAnAccountWithMovieCategoryList(totalAccountData: String): List<AccountWithMovieCategoryList>
However Room will retrieve ALL the MovieCategories but you want to be able to specify a LIMITed number of MovieCategories for an Account, so a means is required to override Room's methodology of getting ALL mapped/associate objects.
To facilitate this then a function with a body can be used to a) get the respective AccountData and b) to then get the MovieCategory list according to the account, via the mapping table with a LIMIT specified. Thus 2 #Query functions to doe the 2 gets invoked by the overarching function.
So to get the AccountData:-
#Query("SELECT * FROM accountData WHERE totalAccountData=:totalAccountData")
fun getSingleAccount(totalAccountData: String): AccountData
And then to get the limited MovieCategories for an AccountData via (JOIN) the mapping table :-
#Query("SELECT movieCategory.* FROM accountMovieMap JOIN movieCategory ON accountMovieMap.MovieCategoryMap = movieCategory.id WHERE accountMovieMap.accountDataMap=:totalAccountData LIMIT :limit")
fun getLimitedMovieCategoriesForAnAccount(totalAccountData: String,limit: Int): List<MovieCategory>
And to put it all together aka the overarching function:-
#Transaction
#Query("")
fun getAccountWithLimitedMovieCategoryList(totalAccountData: String,categoryLimit: Int): AccountWithMovieCategoryList {
return AccountWithMovieCategoryList(
getSingleAccount(totalAccountData),
getLimitedMovieCategoriesForAnAccount(totalAccountData,categoryLimit)
)
}
Note the above code has only been compiled (so Room processing sees no issues), as such it is in-principle code
You say Best, this is opiniated and is not the best as a better way would be to utilise SQLite's more efficient handling of INTEGER primary keys.
categoryLimit i.e. an Int passed could be dynamically via the UI selected or stored and thus preserved. It could be stored in the AccountData by adding a suitable column or it could be stored elsewhere. AccountData would seem the simplest and most apt unless the expectation is that many such account based preferences should exist.
If, for example, an extra column was added to AccountData e.g. :-
#Entity
data class AccountData(
val url: String,
val code: String,
#PrimaryKey(autoGenerate = false)
val totalAccountData: String,
val movieCategoryLimit: Int /*<<<<< to store LIMIT preference */
)
A means of changing the limit would very likely be required such as the following in a/the #Dao annotated interface :-
#Query("UPDATE accountData SET movieCategoryLimit=:newLimit WHERE totalAccountData=:totalAccountData")
fun changeMovieCategoryLimit(totalAccountData: String, newLimit: Int)
A subtle change to the getAccountWithLimitedMovieCategoryList function and the LIMIT is as per the preference:-
#Transaction
#Query("")
fun getAccountWithLimitedMovieCategoryList(totalAccountData: String,categoryLimit: Int): AccountWithMovieCategoryList {
val accountData = getSingleAccount(totalAccountData)
return AccountWithMovieCategoryList(
accountData,
getLimitedMovieCategoriesForAnAccount(totalAccountData,accountData.movieCategoryLimit)
)
}
i.e. rather than using the retrieved AccountData directly, it is retrieved into a val, the val is then used to provide the AccountData and then again to provide the value for the LIMIT.
Additional
As per the comment
.... Isn't it then a one-to-many relation? ....
Then for a 1-M, as previously explained MovieCategory should have a column for storing a unique column. Which I guess is what you index the val accountData: String? to be for.
The ? should never be null (an orphan that is basically useless). Ideally, as the column use would imply selection via this column, the column should be indexed. As the intended use is as a foreign key, then although not required defining it as a foreign and enforcing referential integrity makes sense (and also goes towards describing/comment the column as a foreign key). Then the MovieCategory class could be
:-
/* MovieCategory modified for 1-m (1 account many categories)*/
#Entity(
foreignKeys = [
ForeignKey(
entity = AccountData::class,
parentColumns = ["totalAccountData"],
childColumns = ["accountData"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
)
]
)
#Parcelize
data class MovieCategory(
#PrimaryKey(autoGenerate = false)
val id: String,
val number: Int,
val title: String,
#ColumnInfo(index = true) /* added as likely to used for selecting rows */
val accountData: String /* should NEVER by null */
) : Parcelable
Assuming that AccountData includes the movieCategoryLimit (as explained above) then a POJO for retrieving the Account and the LIMITED list of MovieCategories would be required (a little different to the m-m equivalent) which could be:-
data class AccountDataWithMovieCategories(
#Embedded
val accountData: AccountData,
#Relation(
entity = MovieCategory::class,
parentColumn = "totalAccountData",
entityColumn = "accountData"
)
val movieCategories: List<MovieCategory>
)
i.e. no association table involved
The same issue exists that Room will retrieve ALL MovieCategories. So the following is not what you want:-
/* Note will get ALL MovieCategories (even with join and LIMIT) */
#Transaction
#Query("SELECT * FROM accountData WHERE totalAccountData=:totalAccountData")
fun getAccountWithMovieCategories(totalAccountData: String): List<AccountWithMovieCategoryList>
Rather you want to have:-
#Query("SELECT * FROM movieCategory WHERE accountData=:totalAccountData LIMIT :limit")
fun getLimitedMoviesCategoriesPerAccount(totalAccountData: String, limit: Int): List<MovieCategory>
i.e. directly accessing the MovieCategory rather than via the JOIN on the association table in the m-m version
An then finally the overarching function:-
#Transaction
#Query("")
fun getAccountWithLimitedMovieCategories(totalAccountData: String, limit: Int): AccountDataWithMovieCategories {
val accountData = getSingleAccount(totalAccountData)
return AccountDataWithMovieCategories(
accountData,
getLimitedMovieCategoriesForAnAccount(
accountData.totalAccountData,
accountData.movieCategoryLimit /* the limit as stored in the account */
)
)
}
#MikeT
Sorry for the late response...(I am using an answer because It's to long for a comment and editing my question would be messy)
I was nearly able to get everything I wanted, using this way (example series categories):
I saved the fetched categories (using retrofit) in a new POJO, named SeriesCategory:
#Entity(
foreignKeys = [
ForeignKey(
entity = AccountData::class,
parentColumns = ["totalAccountData"],
childColumns = ["accountData"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
)
]
)
#Parcelize
data class SeriesCategory(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
#ColumnInfo(index = true)
var accountData: String,
var favorite: Boolean,
val idByAccountData: String
) : Parcelable
using...
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSeriesCategory(seriescategory: List<SeriesCategory>)
val mappedSeriesCategoryList = seriescatresponse.js.map { SeriesCategorybyAccount(it.id, it.title, totalAccountData, isFavorite, "${it.id}$totalAccountData") }
lifecycleScope.launch {
mappedSeriesCategoryList.forEach { dao.insertSeriesCategory(mappedSeriesCategoryList) }
}
In the Dao & SeriesCategoryFragment I use following code to get all categories from this specific account:
#Query("SELECT * FROM seriesCategory WHERE accountData=:totalAccountData")
fun getSeriesCategoriesPerAccount(totalAccountData: String): LiveData<List<SeriesCategory>>
viewModel.getSeriesCategoryByAccount(totalAccountData, this#SeriesCategoryFragment.requireActivity()).observe(viewLifecycleOwner) {
seriesCategoryAdapter.submitList(it)
}
As I am using a checkbox in the SeriesCategoryFragment-recyclerview I managed the checkbox in the adapter and used following code in the Dao & SelectedSeriesCategoryFragment:
#Query("SELECT * FROM seriesCategory WHERE accountData=:totalAccountData AND favorite = 1")
fun getSelectedSeriesCategoriesPerAccount(totalAccountData: String): LiveData<List<SeriesCategory>>
viewModel.getSelectedSeriesCategoryByAccount(totalAccountData, this#SeriesSelectedFragment.requireActivity()).observe(viewLifecycleOwner) {
selectedSeriesCategoryAdapter.submitList(it)
}
Thats working fine, when in the SeriesCategoryFragment a checkbox is checked(favorite = true), the category is "send" to the SelectedSeriesCategoryFragment, if it's unchecked (favorite = false) it's removed.
But at the moment I am not able to save this in my room database. So when I have for example following categories:
CategoryA, CategoryB, CategoryC, CategoryD
The user checks CategoryA and CategoryC using the checkbox, this two categories are then displayed in the SelectedSeriesCategoryFragment-recyclerview. When he unchecks the checkbox the category is removed. That's all ok.
In the same clicklistener I am using also #Update with (it = SeriesCategory):
viewModel.updateSeriesCategory(it, this#SeriesCategoryFragment.requireActivity())
Restarting the app the database shows me favorite = 1 (for true) on the categories that where checked before the restart. But the checkboxes are unchecked - an the #Query I use in the SelectedSeriesCategoryFragment (see above) doesn't seem to work anymore -> means, it is empty.
On a second restart the database shows all categories as favorite = 0 (for false) - probably because the checkbox was empty before the second restart. So as I understand, the idea I have should work - but only if the checked Checkboxes stay checked also after the restart. Can I handle this somehow with room?
Or is this a completely recyclerview-specific problem?
Eventually, my Viewholder in the Adapter looks like this:
inner class ViewHolder(val binding: RvItemSeriescategoryBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(category: SeriesCategory) {
binding.apply {
rvItemSeriescategory.text = category.title
checkboxSeriescategory.isChecked = category.favorite
checkboxSeriescategory.setOnClickListener {
if (checkboxSeriescategory.isChecked) {
category.favorite = true
onClickListener.onClick(category)
}
if (!checkboxSeriescategory.isChecked)
category.favorite = false
onClickListener.onClick(category)
}
if (category.favorite == true){
checkboxSeriescategory.isChecked = true
}
}
}
}

Unable to get correct values in Room by using #Relation

I have three tables in my Room database, my query should return a list of products that contains details about them.
All products are inserted based on a documentId so when the query is done I need to get in the list only the items for that documentId.
The DAO looks like this:
#Transaction
#Query("SELECT * FROM document_products dp LEFT JOIN products ON products.id = dp.productId LEFT JOIN products_barcodes ON products_barcodes.barcode = dp.productId WHERE dp.documentId = :documentId AND (products_barcodes.barcode = dp.productId OR products.id = dp.productId) ORDER BY timestamp ASC")
fun getProductsWithDetails(documentId: Long): Flow<List<DocumentProductWithDetails>>
And if I test the query in a table like this:
Where documentId is 5 the query returns the correct values:
But those values are incorrect in the application probably cause of #Relation in DocumentProductWithDetails but I'm unable to find the issue, in facts inside the application the data is shown as this:
So as the item with productId is saved three times it is showing the last item instead of the one related to documentId
The data class which contains #Relation annotation looks like this:
#JsonClass(generateAdapter = true)
data class DocumentProductWithDetails(
#Relation(
parentColumn = "id",
entityColumn = "productId"
)
var product: DocumentProduct,
#Embedded
var details: ProductWithBarcodes?
)
Where DocumentProduct and ProductWithBarcodes:
data class DocumentProduct(
#PrimaryKey(autoGenerate = true)
var id: Long,
var productId: String,
var quantity: Float,
var orderQuantity: Float,
var purchase: Float,
var documentId: Long,
var labelType: String?,
var timestamp: Long?
)
data class ProductWithBarcodes(
#Embedded
var product: Product,
#Relation(
parentColumn = "id",
entityColumn = "productId"
)
var barcodes: List<Barcode>
)
So as the item with productId is saved three times it is showing the last item instead of the one related to documentId
IF any columns of the query have the same column name, the values assigned may(will be) inconsistent in that the value of the last column, of the repeated name, will be the value assigned to all the other columns that have the same name (etc).
I would suggest that the fix is to use unique column names. e.g. instead of productId in the Barcode that you use a column name more indicative of the use of the value, it could be considered as a map to the product so perhaps barcode_productIdMap.
it is not the query that is at fault but how Room handles retrieving and assigning values.
Consider your second image (with some marking):-
The above is explained in more detail, with examples, in this answer
Which is the correct id, productId, quantity (Rhetorical).
How is Room supposed to know what goes where? (Rhetorical)
Consider that the following query extracts exactly the same data (but with the data columns in a different sequence):-
#Query("SELECT products.*,products_barcodes.*,document_products.* FROM document_products dp LEFT JOIN products ON products.id = ....
How is Room meant to cope with the different order (should the correct productId be the first or the second (Rhetorical)).
with unique column names the column order is irrelevant as Room then knows exactly what column is associated with what field, there is no ambiguity.
with unique columns tablename.columname can be reduced to just the column name in the SQL, so the SQL can be simplified.

Room Android ignores #Query conditions in Dao class (Strange)

I am posting this because same issue is already there on stackoverflow but no solution on this. I am using Room library for db operations. I have created data classes with #Embedded and #Relation with other tables. Now the issue is when I put join queries with multiple where conditions on main as well as joined tables, it returns all/incorrect data of joined tables. This shows it ignores the conditions I have put in a DAO class query. Important thing is when I run the same query on a database externally (using stetho in chrome) it works as expected. Please help me with this as this is highly critical issue. Room version: 2.4.0
This is the data class:
data class ProductFull{
#Embedded val product: ProductMaster,
#Relation(
entity = ProductZone::class,
parentColumn = "productId",
entityColumn = "productId",
)
var productZone: ProductZone? = null,
}
This is the DAO class method:
#Query("select * from ProductMaster as pm inner join ProductZone as pz on pz.productId = pm.productId where pz.zoneId = 3")
abstract suspend fun getTempProducts(): List<ProductFull>
Above query returns data in productZone field of data class having zoneId = 1. Whereas it should only return zones having zoneId = 3.
When using #Relation room builds the underlying query(ies) to get ALL children (ProductZones) for each parent (ProductMaster) that the query selects.
A convenience annotation which can be used in a POJO to automatically fetch relation entities. When the POJO is returned from a query, all of its relations are also fetched by Room.
https://developer.android.com/reference/kotlin/androidx/room/Relation
A get-around is two have 2 dao's one that selects the parents and the other that selects the required children and a function (use an abstract class rather than an interface for the Dao's) that gets the parents using the first query and then for each parent gets the required children using the second query.
The function should be annotated with #Transaction, to allow this also annotate it with #Query("")
You would want something like:-
#Transaction
#Query("SELECT * FROM productmaster JOIN productzone on productmaster.productId = productzone.productId WHERE productzone.zoneId = 3")
abstract fun getTempProducts(): List<ProductFull>
#Query("SELECT * FROM productzone WHERE productId=:productId AND zoneId=3")
abstract fun getTempZone(productId: Long): ProductZone
#Transaction
#Query("")
fun buildFullProducts(): List<ProductFull> {
var rv = getTempProducts()
for (pf: ProductFull in rv) {
pf.productZone = getTempZone(pf.product.productId!!)
}
return rv
}
and use the buildFullProducts function to retrieve the list of ProductFull's

RoomDB - JOIN query for which the result is a list of data types with a field whose value is a multiplication of 2 tables fields

Let's say I have the following entities for my RoomDB:
enum class Type {
FOO,
BAR
}
#Entity(
tableName = "amount",
primaryKeys = ["id", "type"]
)
data class Amount(
val id: Long,
val type: Type,
val amount: Int
)
#Entity(
tableName = "value",
primaryKeys = ["id", "valueType"]
)
data class Value(
val id: Long,
val valueType: Type,
val value: Int
)
What I want to do is somehow, with a SQL query (or RoomDB annotations ideally...) query the amount table, and join each amount with the corresponding value row(s) from the value table (using the type from amount to cross reference the valueType field on the value table) by multiplying the amount field with the value field to end up with an object like this:
data class ValueOfAmount(
val type: Type,
val valueOfAmount: Int // amount * value
)
I can think of a way to do this, but it requires doing some of the JOIN "logic" in my repo code layer, when I'd prefer to do this at the query layer instead (if at all possible).
Create a joined data class like so:
data class AmountWithValue(
#Embedded
val amount: Amount,
#Relation(
parentColumn = "type",
entityColumn = "valueType"
)
val value: Value
)
Expose a function from my dao to retrieve the joined data:
#Query("SELECT * from amount")
suspend fun getAmountsWithValues() : List<AmountWithValue>
Consume this function, and map the results to ValueOfAmount instances like so:
val valueOfAmounts = dao.getAmountsWithValues().map { amountWithValue ->
ValueOfAmount(
amountWithValue.amount.type,
amountWithValue.amount.amount * amountWithValue.value.value
)
}
// Do stuff with value of amounts
What I'd like to know is if there is some way to encode that mapping code into the QUERY itself (either via SQL or, even better, if RoomDB has some annotations that support this kind of complex query as annotations on my data types - similar to how it let's me define relationships for simple JOIN operations).
I believe that the following may be what you want:-
First a Class to be joined :-
data class AmountWithCalculatedValue(
#Embedded
val amount: Amount,
val calculatedValue: Int
)
And then a Dao :-
#Query("SELECT *,(amount * value) AS calculatedValue FROM amount JOIN value ON amount.type = valueType")
fun getAmountWithCalculatedValue(): List<AmountWithCalculatedValue>
if you wanted the Value as well it's a little more complicated due to duplicate/ambiguous columns but you could use:-
data class AmountWithCalculatedValue(
#Embedded
val amount: Amount,
val calculatedValue: Int,
#Embedded(prefix = "_value_")
val value: Value
)
With :-
#Query("SELECT amount.id, amount.amount, amount.type,value.id AS _value_id, value.id AS _value_value, value.valueType AS _value_valueType,(amount * value) AS calculatedValue FROM amount JOIN value ON amount.type = valueType")
fun getAmountWithCalculatedValue(): List<AmountWithCalculatedValue>
or :-
#Query("SELECT amount.*,value.id AS _value_id, value.id AS _value_value, value.valueType AS _value_valueType,(amount * value) AS calculatedValue FROM amount JOIN value ON amount.type = valueType")
fun getAmountWithCalculatedValue(): List<AmountWithCalculatedValue>
that is using the #Embedded's prefix parameter is saying that the values will be prefixed with the prefixed with the prefix so you have to us AS clauses to disambiguate the respective columns. As no prefix is used on the amount columns then amount.* can be used (2nd dao).
I'd say that for 1-1 relationships using #Relationship (as opposed to #Embedded) is perhaps a little less efficient than using a JOIN. As the way Room works it gets the Parent's from the given query and then retrieves the #Relation from the Parent via a separate query and hence why it recommends #Transaction.

Search query using Android room relation

In Android room relation, is it possible to use search query using the property of the related table. Below is my table structure. In this i am relating transaction with payment and lines(transaction items). I have an search field in my UI where the user could search using payment amount which is inside payment table. How to form a query to access the properties of payment table.
class TransactionWithPaymentAndLines(
#Embedded
var transactions: Transactions? = null,
#Relation(
parentColumn = "id",
entityColumn = "transactionId",
entity = Payment::class
)
var payments: List<Payment> = listOf(),
#Relation(
parentColumn = "id",
entityColumn = "transactionId",
entity = TransactionLines::class
)
var transactionLines: List<TransactionLines> = listOf()
)
Ideal way is to query multiple related tables is to create a View. A view combines data from two or more tables using join.
In Android, using Room Persistance library, you can create such a view, and then you can query the fields of view. This is how you can do it:
Suppose, you have tables:
User: id, name, departmentId
Department: id, name
Create a View:
#DatabaseView("SELECT user.id, user.name, user.departmentId," +
"department.name AS departmentName FROM user " +
"INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(
val id: Long,
val name: String?,
val departmentId: Long,
val departmentName: String?
)
Add View to Database:
#Database(entities = arrayOf(User::class),
views = arrayOf(UserDetail::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDetailDao(): UserDetailDao
}
Create a DAO:
#Dao
interface UserDetailDao {
#Query("SELECT * FROM UserDetail")
fun loadAllUserDetails(): Array<UserDetail>
}
Now, you can query a View using this DAO.
Absolutely possible, you can use #Query in your DAO class please read Room Database Documentation
Examples of #Query
#Query("SELECT * FROM user")
List<User> getAll();
#Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
#Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
use #DB or #Query.
That should perfectly work...
#Query("SELECT * FROM TABLE_NAME")
List<Identifier> getAll();

Categories

Resources