Return Map object by DAO method - android

I would like to have method like this in my dao object
#Query("SELECT c.name, sum(p.value) FROM payments p, paymentCategories c WHERE p.categoryId = c.id GROUP BY c.name")
fun getCategoryStats(): Map<String, Float>
but i get the error
error: Not sure how to convert a Cursor to this method's return type
public abstract java.util.Map
Is it possible to change it to working version?
So it can be different type to return but the main conditions are
It must be only one query in db
I would like to avoid extra code like creating additional data structure only for this method

I'm little late for a party but maybe someone will still search for it.
From Room 2.4 we are able to use multimap return type with annotation #MapInfo which allow us to define the mapping for key and value.
In your case it will be sth like this:
#MapInfo(keyColumn = "name", valueColumn = "sum")
#Query("SELECT c.name AS name, sum(p.value) AS sum FROM payments p, paymentCategories c WHERE p.categoryId = c.id GROUP BY c.name")
fun getCategoryStats(): Map<String, Float>
More info:
https://developer.android.com/training/data-storage/room/accessing-data#multimap

While I don't think this can be done in the Dao it can easily be done when querying the LiveData in the repository or the viewModel (or where ever you are querying the list) using a transformation. Since I don't know your datanames I'm using made up ones:
val categoryStatsMap: LiveData<Map<String, Float>> =
Transformations.map(
database.categoryStatsDao.getCategoryStats()) {it ->
it.map {it.key to it.value}.toMap()
}
'it.key' and 'it.value' are the fields in the entity object that you want to use as... key and value pairs in the map.
Transformations give you a live data object based on another live data object. I don't know what the overhead is, but I assume it shouldn't be too big.

Related

Obtaining integer from room query math result

I'm trying to obtain the result of a subtraction between two rows in the database. Users specify the conditions on spinners (populated with the "model" column), press a button and the query is launched.
Spinners are properly saving the position into sharedpreferences and later obtaining it.
Button function:
public int value;
//later on
TextView converter = findViewById(R.id.converter);
AppExecutors.getInstance().diskIO().execute(() -> {
LiveData<Integer> value = mDb.personDao().loaddifferconstants(spinA, spinB);
converter.setText(""+value); //quick dirty method
});
Dao
#Query("SELECT t1.const - t2.const AS result FROM person t1 JOIN person t2 WHERE t1.model == :spinA AND t2.model == :spinB")
LiveData<Integer> loaddifferconstants(String spinA , String spinB);
The query does work in DBBrowser, as a direct sql query. So I guess the error lies on how the result is processed into an integer. I tried listing the result, using both livedata integer, int, list... trying to pass it as a String... Failed.
Update 1:
Integer doesn't work either.
Actually Integer count doesn't work either, with the Dao being
#Query("SELECT COUNT(*) FROM PERSON")
int count();
Thank you
LiveData<Integer> value = mDb.personDao().loaddifferconstants(spinA, spinB);
converter.setText(""+value); //quick dirty method
value is a LiveData. This will cause the query to be executed asynchronously. By the next statement, that query will not have completed, and the LiveData will not have the query result.
Either:
Remove LiveData from loaddifferconstants() and have it simply return Integer, so the query will be executed synchronously, or
Consume the LiveData properly, by registering an observer
Since you seem to by trying to call those two lines inside your own background thread, I recommend the first approach: get rid of the LiveData. That would give you:
#Query("SELECT t1.const - t2.const AS result FROM person t1 JOIN person t2 WHERE t1.model == :spinA AND t2.model == :spinB")
Integer loaddifferconstants(String spinA , String spinB);

matching multiple title in single query using like keyword

matching multiple title in single query using like keyword
I am trying to get all records if that matches with given titles.
below is the structure of database please see
database screenshot
when i pass single like query it returns data
#Query("SELECT * FROM task WHERE task_tags LIKE '%\"title\":\"Priority\"%'")
when i try to generate query dynamically to search multiple match it return 0 data
val stringBuilder = StringBuilder()
for (i in 0 until tags.size) {
val firstQuery = "%\"title\":\"Priority\"%"
if (i == 0) {
stringBuilder.append(firstQuery)
} else stringBuilder.append(" OR '%\"title\":\"${tags[i].title}\"%'")
}
this is function I have made
#Query("SELECT * FROM task WHERE task_tags LIKE:tagQuery ")
fun getTaskByTag(stringBuilder.toString() : String): List<Task>
The single data is fine. However, you simply cannot use the second method.
First you are omitting the space after LIKE,
Then you are omitting the full test i.e. you have task_ tags LIKE ? OR ?? when it should be task_tags LIKE ? OR task_tags LIKE ?? ....
And even then, due to the way that a parameter is handled by room the entire parameter is wrapped/encased as a single string, so the OR/OR LIKE's all become part of what is being searched for as a single test.
The correct solution, as least from a database perspective, would be to not have a single column with a JSON representation of the list of the tags, but to have a table for the tags and then, as you want a many-many relationship (a task can have many tags and a single tag could be used by many tasks) an associative table and you could then do the test using a IN clause.
As a get around though, you could utilise a RawQuery where the SQL statement is built accordingly.
As an example:-
#RawQuery
fun rawQuery(qry: SimpleSQLiteQuery): Cursor
#SuppressLint("Range")
fun getTaskByManyTags(tags: List<String>): List<Task> {
val rv = ArrayList<Task>()
val sb=StringBuilder()
var afterFirst = false
for (tag in tags) {
if (afterFirst) {
sb.append(" OR task_tags ")
}
sb.append(" LIKE '%").append(tag).append("%'")
afterFirst = true
}
if (sb.isNotEmpty()) {
val csr: Cursor = rawQuery(SimpleSQLiteQuery("SELECT * FROM task WHERE task_tags $sb"))
while (csr.moveToNext()) {
rv.add(
Task(
csr.getLong(csr.getColumnIndex("tid")),
csr.getString(csr.getColumnIndex("task_title")),
csr.getString(csr.getColumnIndex("task_tags"))))
// other columns ....
}
csr.close()
}
return rv
}
Note that the complex string with the embedded double quotes is, in this example, passed rather than built into the function (relatively simple change to incorporate) e.g. could be called using
val tasks1 = taskDao.getTaskByManyTags(listOf()) would return no tasks (handling no passed tags something you would need to decide upon)
val tasks2 = taskDao.getTaskByManyTags(listOf("\"title\":\"Priority\""))
val tasks3 = taskDao.getTaskByManyTags(listOf("\"title\":\"Priority\"","\"title\":\"Priority\"","\"title\":\"Priority\"")) obviously the tags would change
Very limited testing has been undertaken (hence just the 3 columns) but the result of running all 3 (as per the above 3 invocations) against a very limited database (basically the same row) results in the expected (as per breakpoint):-
the first returns the empty list as there are no search arguments.
the second and third both return all 4 rows as "title":"Priority" is in all 4 rows
the main reason for the 3 search args was to check the syntax of multiple args, rather than whether or not the correct selections were made.
The resultant query of the last (3 passed tags) being (as extracted from the getTaskaByManyTags function):-
SELECT * FROM task WHERE task_tags LIKE '%"title":"Priority"%' OR task_tags LIKE '%"title":"Priority"%' OR task_tags LIKE '%"title":"Priority"%'

Using .addOnSuccessListener to return a value for a private method

Good day. Is there some way that I could implement this one?
val db = Firebase.firestore
val userID = Firebase.auth.currentUser!!.uid
val infoRef = db.collection("user").document(userID).collection("profile").document("info")
infoRef.get()
.addOnSuccessListener {document ->
if(document != null){
//get the data as cast to hashmap
val data = document.data as HashMap<*, *>
//get the username field and set text for greet user as the same value inside firestore
val username = data["username"] as String
tv_greet_user.text = "Hello, $username"
}
}
//extract the code above as a new method called "getUsername()"
val username : String = getUsername()
tv_greet_user.text = "Hello, $username"
Yes, you can create a function that looks like this:
fun getUsername(data: HashMap<String, Any>) = data["username"] as String
And inside your callback simply call:
val username = getUsername(data)
Is there a way to extract the whole block into a separate method? So that in the onCreate method, I could simply change the TextView into something like: val username = getUsername() tv_greet_user.text = "Hello, $username"
Edit:
As also Frank van Puffelen mentioned in his you cannot return the result of an asynchronous operation as a result of a method. Since you are using Kotlin programming, please note that there is actually o solution. I wrote an article called:
How to read data from Cloud Firestore using get()?
In which I explained four ways in which you can get data from Firestore. So if you are willing to use Kotlin Coroutines, then things will be much simpler.
Data is loaded from Firestore (and most modern web/cloud APIs) asynchronously. Since it may take some time before the data is available, your main code continues running while the data is being loaded. Then when the data is available, your success listener callback is called with that data.
This unfortunately means that it is impossible to return the value from the database in a function, because by the time the return statement runs, the data hasn't been loaded yet.
And that's also precisely why infoRef.get() in your code doesn't simply return the value from the database, but requires that you pass in a callback function that it invokes when the database is available. Sure, your code would be a lot simpler if get() would immediately return the value from the database, but it can't do that because the data needs to be loaded from the network.
I recommend reading:
The Kotlin docs on asynchronous programming techniques
Why does my function that calls an API return an empty or null value?
How to return a DocumentSnapShot as a result of a method?
How to check a certain data already exists in firestore or not

Can you chain multiple whereEqualTo operations in one query in pieces in Firestore

In my app, I have a multiple-choice dialog with various filter options that the user should be able to choose in order to filter the database based on the rarity field of the documents. Since there are many options in the filter dialog, covering each case by hand would take ages if we take into account all the possible combinations of the filters. With that in mind, I tried creating a starting query as you can see below and then I iterate through the list of filters selected by the user and try to add a whereEqualTo("rarity",filter) operation to the query for each filter. I noticed that you can't concatenate queries like with normal variables e.g. var i += 5 so i would like to know if there is any solution to this kind of issue. Can you actually apply multiple whereEqualTo operations in the same query in steps/pieces without overriding the previously applied operations on that same query?
Here's what I've tried after receiving the filters selected by the user in my FilterActivity.kt class:
class FilterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_filter)
val db = FirebaseFirestore.getInstance()
val filters:ArrayList<String>? = intent.getStringArrayListExtra("filterOptions")
Log.d("FilterActivity", "filter options $filters")
var query = db.collection("Cards").orderBy("resID")
for(filter in filters!!) {
query = query.whereEqualTo("rarity",filter)
}
query.get().addOnSuccessListener { querySnapshot ->
if(querySnapshot.isEmpty) Log.d("FilterActivity","is empty")
for(doc in querySnapshot.documents) {
Log.d("FilterActivity", "${doc.getString("name")} - ${doc.getString("rarity")}")
}
}
}
}
Basically you are trying a do OR operation, where you are retrieving all documents, in which rarity fields matched any of the value in array.
You are try new firebase whereIn operation where you can pass array of values, but theres a limitation of max 10 values in filter
FirebaseFirestore.getInstance()
.collection("Cards")
.orderBy("resID")
.whereIn("rarity",filters!!.toList())
.get().addOnSuccessListener { querySnapshot ->
if (querySnapshot.isEmpty) Log.d("FilterActivity", "is empty")
for (doc in querySnapshot.documents) {
Log.d("FilterActivity", "${doc.getString("name")} - ${doc.getString("rarity")}")
}
}
filters arraylist can contain max 10 values
Can you chain multiple whereEqualTo operations in one query in pieces in Firestore
You can chain as may whereEqualTo operations as you need.
The problem in your code += operator. There is no way you can make an addition/concatenation of two Query objects. To solve this, please change the following line of code:
query += query.whereEqualTo("rarity",filter)
to
query = query.whereEqualTo("rarity",filter)

Android Room - How to migrate when a nested object model is changed?

I have a database schema set up using Android Room, Dao, and Entity classes set up as POJOs. Except the POJO entity isn't so "plain" and that it actually holds a reference to another object. I thought this was a great idea at the time as it allowed me more flexibility in changing the object and using it in other places in the app and only saving to the database as needed.
The problem I'm facing now is that the migration guideline only mentions how to migrate the database by altering the SQL, but I changed the object itself. My typeconverter class simply converts the object to and from a string.
Because it's being saved as a long string I know I essentially have to do a simple REPLACE(string, old_string, new_string) in the SQL
migration code block with the updated object being the new string. How can I retrieve the old objects and update values before running the replace SQL command in the migration block?
UPDATE: I'm using GSON in my typeconverter class to change the object to a string, so the solution that comes to mind is to simply download the old object and upload the new one with the added fields. Only problem is that you can't access the database and download the json, convert it to the object, add the new data fields, then reconvert to a new json string.
I'm lucky I'm not at scale yet because this would be a tricky thing to do for so many users. (So I recommend that anyone reading this not do what I did and implement object nesting. It's easier to convert the Entry objects to the other portable objects instead of nesting when it comes to updating the data you want saved.)
I think if you already did what I did and can't go back, the best bet is to simply create the new portable object and make new typeconverter functions for that one and add the SQL COLUMN for the new object. The problem then lies in how you then retrieve those objects from the Entry Dao, which will cause a lot more code to write and possible errors to debug if not done carefully.
Long story short, if anyone is reading this, DO NOT nest objects in Room DBs on Android unless you are 100% sure it's a final form of your model... but is there such a thing anyways?
I just ran into this issue, but fortunately I only needed to add a new key/value pair to a "flat" object model. So hopefully my answer can be expanded on to fully answer #Mr.Drew question.
Assuming you have a table town with a column star_citizen that is the object model being typeconverted:
{"name":"John", "age":30, "car":false}
and you want to update the object to have an extra property "house": true
you could add a migration to your App's Room Database class like this (Kotlin example):
#Database(entities = [Town::class], version = 2, exportSchema = true)
#TypeConverters(DataConverters::class)
abstract class AppDatabase : RoomDatabase() {
abstract val sharedDao : SharedDao
companion object {
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
val cursor = database.query("SELECT * FROM `town`")
// iterate through each row in `town`, and update the json
// of the StarCitizen object model
cursor.moveToFirst()
while (!cursor.isAfterLast) {
val colIdIdx = cursor.getColumnIndex("id")
val id = cursor.getInt(colIdIdx)
val colStarCitizenIdx = cursor.getColumnIndex("star_citizen")
val rawJson = cursor.getString(colStarCitizenIdx)
val updatedRawJson = starCitizenModelV1ToV2(rawJson)
database.execSQL("""UPDATE town SET star_citizen ='${updatedRawJson}' WHERE ID = $id""")
cursor.moveToNext()
}
}
}
//[...]
private fun starCitizenModelV1ToV2(rawJson: String): String {
val rawJsonOpenEnded = rawJson.dropLast(1)
val newProperty = "\"house\":true"
return "$rawJsonOpenEnded,$newProperty}"
}
}
}

Categories

Resources