Android RoomDB dynamic select parameter - android

in order to cut down on repeated code in my Dao I'm trying to consolidate 4 similar calls into one. I have 4 different queries that are exactly the same except they SELECT a different value from the row. To determine what value I select, I want to pass in a variable which is a string, which corresponds to the entity attribute value:
#Query("""
SELECT :infoType
FROM entry_log
WHERE date >= date(:startDate)
AND date < date(:endDate)
ORDER BY datetime(date) DESC
""")
fun getEntryInfoInDateRange(startDate: String, endDate: String, infoType: String): Observable<List<Int?>>
When I call this, I get back a list with the correct number of elements, but all of them are 0 (which is incorrect)
However, if I have:
SELECT infotypeone
FROM entry_log
WHERE date >= date(:startDate)
AND date < date(:endDate)
ORDER BY datetime(date) DESC
""")
fun getEntryInfoInDateRange(startDate: String, endDate: String, infoType: String): Observable<List<Int?>>
I will be given a list with the correct data (they aren't all 0)
Anyone know if there's some limitation to using parameters in the SELECT field? The parameters in the WHERE/AND fields work just fine

In theory there is one way to do something like you want with #RawQuery. This method has some limitations (for example, you shouldn't expect for type converters to work, and I'm not sure it works seamless with RxJava/LiveData, may be there are others?), but still you can try.
From documentation:
If you know the query at compile time, you should always prefer Query since it validates the query at compile time and also generates more efficient code since Room can compute the query result at compile time
On the other hand, RawQuery serves as an escape hatch where you can build your own SQL query at runtime but still use Room to convert it into objects.
In your Dao there would be two methods (you can play with Observable, but I'm not sure it can digest that):
#RawQuery
fun getEntryInfoInDateRangeTemplate(query: SupportSQLiteQuery): Observable<List<Int>>
fun getEntryInfoInDateRange(startDate: String, endDate: String, infoType: String):Observable<List<Int?>> {
val query =
"""
SELECT infoType
FROM entry_log
WHERE date >= date(?startDate)
AND date < date(?endDate)
ORDER BY datetime(date) DESC
""".replace("infoType", infoType)
return getEntryInfoInDateRangeTemplate(SimpleSQLiteQuery(query, arrayOf(startDate, endDate)))
}
And as a result you can call the second method from Repository/ViewModel.

Related

Changing primaryKeys order changes the order of the data saved in ROOM

I have a situation where I need some information about my results, in my code I have this data class/Room's entity.
#Entity(primaryKeys = ["searchId","page","pr_id"])
data class ProductResponse(
var searchId: Int,
val page: Int,
#Embedded(prefix = "pr_")
val products: ProductSearchFormatted
)
Where you can see in the Android Studios App Inspector:
The data is saved as spected in ROOM, when I try to load it from ROOM:
#Query(
"SELECT * FROM PagedSearchResponse WHERE searchId ==:input"
)
fun loadPagedSearchResponse(input: Int): PagingSource<Int, ProductResponse>
I just need the data in the same order that was previously saved, and got the data in different order (ordered by pr_id):
I found out that if I change the primaryKeys order, like
#Entity(primaryKeys = ["pr_id","searchId","page"])
data class ProductResponse(
var searchId: Int,
val page: Int,
#Embedded(prefix = "pr_")
val products: ProductSearchFormatted
)
Now the data's order from ROOM is correct.
Why does this happen? Does the primaryKeys order matter?
Changing primaryKeys order changes the order of the data saved in ROOM
NO it does not, the data is ALWAYS saved in the order in which it is inserted. What is changing in your case, is the ORDER in which the data is extracted. That is because you aren't saying in what ORDER you want the data to be extracted and are leaving that choice to the query planner.
Why is this happened?, does the primaryKeys order matter?.
Yes it can do, especially in the absence of other indexes. Certainly pr_id before search_id will make a difference as the order within the index will be different and that as the WHERE clause is on the search_id then it is likely that the primary key index will be used (as in both cases search_id is an initial column (see the links below))
The query planner is an AI that tries to pick the fastest and most efficient algorithm for each SQL statement.
see :-
https://www.sqlite.org/queryplanner.html
https://www.sqlite.org/optoverview.html
https://www.sqlite.org/queryplanner-ng.html
If you want data to be in ORDER then you should specify an ORDER clause (ORDER BY ....). That is the only way to guarantee an ORDER. Assuming an ORDER without an ORDER clause will very likely result in issues.
Saying that using pr_id prior to search_id makes the composite (multiplte column) index likely to be more beneficial as the pr_id (according to the data shown) is less repeated than the search_id.
The way I fix it, it was simpler than I expected.
In the entity, I added a field called "order", and in my Mediator (I'm using Paging 3) basically did this:
list.mapIndex { s,t ->
ProductResponse(
...
order = s
...
)
}
That way, I'm following the EXACT order from Backend without the need to modify any primaryKeys.

Android Room combining the result of N queries into a live data list

I have a room database setup and I want to query that database N number of times and combine the results of each query into a live data array to display back to the user.
I'm pretty sure I want to be using MediatorLiveData but every example online has a predefined amount of live data sources it is combining.
I have the following setup:
petDao
#Query("SELECT * FROM pet_table WHERE name LIKE :petName")
fun getPetsByPetName(petName: String): LiveData<Pet>
petRepository
fun getPetsByPetName(petNames: List<String>): LiveData<List<Pet>> {
for (petName: String in petNames) {
val pets = petDao.getPetsByPetName(petName)
// Combine into one live list of pets
}
}
Have you tried this in your DAO?
#Query("SELECT * FROM pet_table WHERE name IN (:petNames)")
fun getPetsByPetName(petNames: List<String>): LiveData<List<Pet>>
It should work with a list of up to 999 arguments. (not sure if the parameter has to be an array, or if the list is fine)
As an extension over SQLite bind arguments, Room supports binding a
list of parameters to the query. At runtime, Room will build the
correct query to have matching number of bind arguments depending on
the number of items in the method parameter.
https://developer.android.com/reference/androidx/room/Query
To me it seems more appropriate for the example you've given.

Room - Select query with IN condition?

Is it possible to use SQLite's IN condition with Room?
I'm trying to select a list of items from my database where the value of a certain column (in this case a TEXT column) matches any one of a set of filter values. That's pretty easily done in SQL and SQLite, by my knowledge, just by adding an IN condition to your SELECT statement (see here). However, I can't seem to make it work with Room.
I keep getting this error:
Error:(70, 25) error: no viable alternative at input 'SELECT * FROM Table WHERE column IN :filterValues'
(where the input to the DAO #Query-annotated method is called filterValues)
I have tried three different methods now:
Passing the argument as a List<String>
Passing the argument as a String[]
And lastly passing the argument as simply a String, but formatted as (value_1, value_2, ..., value_n)
The last one in particular should work easily, as it will (or at least, it should) directly translate to SELECT * FROM Table WHERE column IN (value_1, value_2, ..., value_n), which is the exact way you would manually write out the SELECT if you were just accessing the database directly.
So as I was preparing to submit this, I double-checked a bunch of the stuff I had looked up previously and found the thing I had somehow missed and would have saved this question from being necessary.
As it turns out, both of these options:
Passing the argument as a List<String>
Passing the argument as a String[]
are viable (and you can replace String with any type the database can represent, such as char or int), you simply need to change the syntax in the #Query annotation from this:
#Query("SELECT * FROM Table WHERE column IN :filterValues")
to this:
#Query("SELECT * FROM Table WHERE column IN (:filterValues)")
Easy as pie, right?
Note that the third method above (passing the argument as simply a String, but formatted as (value_1, value_2, ..., value_n)) does not appear to be supported by Room, but that's probably not a bad thing, since that's the hard way.
Since I already had the whole thing typed out, I figured I would leave the question up in case other people are have as much difficulty finding this solution as I did and stumble upon this question.
Hi you can use this query:
#Query("SELECT * FROM user WHERE uid IN(:userIds)")
public abstract List findByIds(int[] userIds);
or
#Query("SELECT * FROM user WHERE uid IN(:userIds)")
public abstract List findByIds(List<Integer> userIds);
Similarly to above answers in Kotlin you can use vararg instead of array or list:
#Query("SELECT * FROM user WHERE uid IN (:userIds)")
fun getUsers(vararg userIds: Int): List<User>?
and use it like repository.getUsers(1, 2, 3).
If needed, to convert vararg to list see https://proandroiddev.com/kotlins-vararg-and-spread-operator-4200c07d65e1 (use *: format(output, *params)).

Hardcode Boolean Query In Room Database

I'm building an Android application that displays a list of potential matches for a user. The user can click on one to like the user, and I save all of those likes locally.
I can write a query to get the list of matches like this:
#Query("SELECT * FROM match WHERE liked = :liked ORDER BY match DESC LIMIT :limit")
fun getMatches(limit: Int = 6, liked: Boolean = true): Flowable<List<Match>>
I've learned that this works fine. However, I don't foresee any scenario where I'll ever set liked to false, and so I'm curious if there is a way to hardcode my Boolean condition? If I try:
#Query("SELECT * FROM match WHERE liked = true ORDER BY match DESC LIMIT :limit")
I get the following error at compile time:
Error:(8, 0) Gradle: error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such column: true)
How can I hard code this Boolean in my query string?
I have also tried:
Wrapping the condition in single quotes
#Query("SELECT * FROM match WHERE liked = 'true' ORDER BY match DESC LIMIT :limit")
SQLite does not have a boolean data type. Room maps it to an INTEGER column, mapping true to 1 and false to 0.
So, I would expect this to work:
#Query("SELECT * FROM match WHERE liked = 1 ORDER BY match DESC LIMIT :limit")
Bear in mind that this behavior is undocumented. However, it shouldn't change — at least not without alarm klaxons sounding — as we'd need to use migrations to deal with any changes.
CommonWare's approach does work and also answers the OPs question directly; however, I'm not a fan of making such an assumption about the database. The assumption should be safe, but it may create unexpected work down the road if Room ever decides to change it's boolean implementation.
I'd suggest that the better approach is to not hardcode the boolean 1 or 0 into the query. If the database is behind a repository, it is still possible for the repository to expose a graceful API. Personally, I think shielding the larger codebase from the database implementation is a good thing anyways.
Dao Method (copied from OP's question)
#Query("SELECT * FROM match WHERE liked = :liked ORDER BY match DESC LIMIT :limit")
fun getMatches(limit: Int = 6, liked: Boolean = true): Flowable<List<Match>>
Repository
class Repository {
public Flowable<List<Match>> getLikedMatches() {
return dao.getMatches(6, true);
}
}
Of course, this is an opinionated option in that it assumes a certain architectural style. However, it does not make assumptions about the internal database. Even without the repository shielding the database, the call can be made into the database by passing true everywhere - also without making assumptions as to the underlying data.
You don't have to compare boolean column to a value. Just use the column value itself as a boolean expression. You can easily change your query to SELECT * FROM match WHERE liked ORDER BY match DESC LIMIT :limit.
If you want to compare to false value you can use following expression: where not liked.
#Query("SELECT * FROM searched_data_table WHERE favourit_history==1 ORDER BY lang_id DESC")
Use this query to search data from the table this will give you data in descending order with respect to your key value

Room - SELECT query, get or default

In SQL Brite, there's a method mapToOneOrDefault. Is there a similar thing in Room?
Say for Model
#Entity(tableName = "users")
data class User(#PrimaryKey val name: String)
and Dao
#Dao
interface UserDao {
#Query("SELECT FROM users where name = :name")
fun getUserByName(name: String): Flowable<User>
}
Not the stream returns nothing for getUserByName("John") if there's no John in DataBase. Is there a way to get a default value, say User("")?
Not the stream returns nothing for getUserByName("John") if there's no
John in DataBase. Is there a way to get a default value, say User("")
There is no default mechanism.
You could change from Flowable<User> to Flowable<List<User>>. In case of no user you will get an empty list back. You can use a map to check and return a default value or filter+switchIfEmpty.
Or you could change from Flowable to Single. With Single, in case of no rows, matching your query, onError will be triggered. You can then implement onErrorReturn or onErrorResumeNext to return a default value
You can use Maybe instead of the Flowable in this case.
Maybe: Conceptually, it is a union of Single and Completable providing the means to capture an emission pattern where there could be 0 or 1 item or an error signaled by some reactive source.
You can then use the operator defaultIfEmpty to map to a new object if the query didn't return a value.
Reference

Categories

Resources