Kotlin / SQLite delete() in batch issue - android

I would like to delete mutiple items from SQLite in batch basing on their ID column.
What I have is a HashMap which contains objects which one of field is pID (unique ID in DB).
So, here's my code:
/*
Delete rows from DB
*/
val selection = "${BaseColumns._ID} = ?"
// Create a list of product ID's to delete
val dbDeletor = dbHelper.writableDatabase
// Temp array to store ID's in String format
val tempIDs = ArrayList<String>()
// Loop through array of items to be deleted
for(i in ProductsRecyclerAdapter.productsToDeleteArray)
tempIDs.add(i.value.pID.toString())
// Perform deletion in DB
val deletedRowsCount = dbDeletor.delete(ProductsEntry.TABLE_NAME, selection, tempIDs.toTypedArray())
// Show snackbar with count of deleted items
Snackbar.make(mainCoordinatorLayout, "Products deleted: $deletedRowsCount", Snackbar.LENGTH_SHORT).show()
Everything works great when I'm deleting only 1 item but if tempIDs array contains 2 or more I'm receiving following Exception:
Too many bind arguments. 3 arguments were provided but the statement needs 1 arguments.
Maybe the reason is that I'm converting pID of type Long into String in order to delete rows in batch? I did not find any other solution. Please take a look and comment.

Your query looks somewhat like that:
DELETE FROM ProductsEntry.TABLE_NAME WHERE BaseColumns._ID = ?
There is only 1 argument ? but you're passing 3 values (IDs). You want to use IN statement instead, and print your params separated by comma:
// IN instead of equal to compare multiple values
val selection = "${BaseColumns._ID} IN (?)"
// your code to obtain IDs here
// .....
// combine all values into single string, ie. 1, 2, 3, 4 and wrap it as an array
val selectionArg = arrayOf(tempIDs.joinToString())
// Perform deletion in DB
val deletedRowsCount = dbDeletor.delete(ProductsEntry.TABLE_NAME, selection, selectionArg)

Related

SQL search multiple values in same field

I have a list of id's to search.
I want to call all data matching the values in this list.
Matching data is returned as a list.
I tried to do this in a loop and using IN but it didn't work.
When I write the values in parentheses one by one, it gives results, but how can I give the data in the list as a parameter?
db.query(_tableName, where: "id IN ('0001','00002','00003')")
When I type in the form, the query takes place. Can I send the incoming array directly into parentheses, not one by one? For example,
db.query(_tableName, where: "id IN (_ids)")
Query method:
Future<List<ContentModel>> getDiscoverContent(List<String> _ids) async {
Database db = await instance.database;
var contents = await db.query(_tableName, where: "id IN ('')");
List<ContentModel> _recommendedList = contents.isNotEmpty
? contents.map((e) => ContentModel.fromMap(e)).toList()
: [];
return _recommendedList;
}
The simplest solution I found:
var contents = await db.query(_tableName,
where:
"id IN('${_ids[0]}','${_ids[1]}','${_ids[2]}','${_ids[3]}','${_ids[4]}',"
"'${_ids[5]}','${_ids[6]}','${_ids[7]}','${_ids[8]}','${_ids[9]}')");
If you have a different solution, feel free to tell me.
You may try like this. This worked for my use case.
List<int> _ids = [1, 2, 3, 4];
// Loop through the _ids list and append quotes to each id
// add the element into a new List called new_Ids
List<String> new_Ids = [];
for (var i = 0; i < _ids.length; i++) {
new_Ids.add("'" + _ids[i].toString() + "'");
}
// use join to make a string from the new list where ids are separated by comma
String ids_Str = new_Ids.join(',');
Use db.query and pass ids string in where itself. Other columns in where condition can be passed in whereArgs if you need.
var resultList = await db.query('your_table_name',
columns: ['id', 'column2'],
where: 'id IN ($ids_Str)');
OR use rawQuery
var resultList = await db.rawQuery("SELECT id, column2 FROM your_table_name WHERE id IN(" + ids_Str + ")");
You may need to be careful of SQL Injection as we are passing dynamic values. Not sure about the best practices here.

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"%'

Failed rename column while join query

I have a query:
#Query("SELECT entries.*, myProfile.myProfileId as my_profile_id FROM Entry as entries LEFT JOIN MyProfile ON entries.user_public_profile_userId = myProfile.myProfileId")
I just need to get value from second(right) table and put it into entitys' field - "my_profile_id" from first table. But nothing works.
this is how field from first table(left) entity look like -
#ColumnInfo(name = "my_profile_id")
val myUserProfileId: Int? = null,
and this is how it look like this field from second table (right)
#PrimaryKey
val myProfileId: Int,
Result is always null, but condition after ON is working because i tried to change from LEFT JOIN to INNER JOIN ad got results, so the only problem here is to map correctly 'myProfileId' into 'my_profile_id'
What am i doing wrong?
Irrespective of the JOIN type using :-
SELECT entries.*, myProfile.myProfileId as my_profile_id FROM Entry as entries LEFT JOIN MyProfile ON entries.user_public_profile_userId = myProfile.myProfileId
Will result in my_profile_id being the same value that is stored in the user_public_profile_userId column. In that sense the JOIN is a waste of time.
I suspect that you may want to get another useful value from the MyProfile table.
Assuming that you have an Entry entity that is along the lines of :-
#Entity
data class Entry(
#PrimaryKey
val id: Long? = 0,
val name: String,
#ColumnInfo(name = "my_profile_id")
val myUserProfileId: Long? = null,
val user_public_profile_userId: Long
)
And an MyProfile entity that is along the lines of :-
#Entity
data class MyProfile(
#PrimaryKey
val myProfileId: Long? = null,
val otherdata: String
)
and that you want to get the value from the otherdata column then you need an additional POJO to combine the data.
As such consider such a POJO EntryPlus :-
data class EntryPlus(
#Embedded
val entry: Entry,
val profileIdFromMyProfile: Long,
val otherdataFromMyProfile: String
)
#Embedded and the following line is saying that you want all the columns/fields from the Entry table
The other two columns will be from elsewhere (satisfied by the query)
So you could have a Query such as :-
#Query("SELECT entries.*, myProfile.myProfileId AS profileIdFromMyProfile, myProfile.otherdata AS otherdataFromMyProfile FROM Entry as entries LEFT JOIN MyProfile ON entries.user_public_profile_userId = myProfile.myProfileId")
fun getMyOtherData(): List<EntryPlus>
i.e. the query is little different BUT importantly uses AS to name the output columns accordingly to suit the names of the fields in the EntryPlus POJO.
also importantly the result is a list of the POJO (EntryPlus).
Example
Consider the following code that:
inserts some data (3 MyProfile rows and 2 Entry rows) and then
extracts All the Entry rows with no JOIN using SELECT * FROM entry and then
extracts using your original query and then
extracts via the POJO
The code is :-
db = TheDatabase.getInstance(this) // Get database instance
dao = db.getAllDao() // get the Dao
dao.deleteAllMyProfile() // Clear the MyProfile table
dao.deleteAllEntry() // Clear the Entry table
// Add some profile rows
dao.insert(MyProfile(1,"myprofile1"))
dao.insert(MyProfile(2,"myprofile2"))
dao.insert(MyProfile(3,"myprofile3"))
// Add some Entry rows (both link to profile1 in this case)
dao.insert(Entry(100,"Entry1",0,1))
dao.insert(Entry(200,"Entry2",0,1))
// Extract 1 All as Entry List (no join)
for(e: Entry in dao.getAll()) {
Log.d("ENTRYINFO(1)","Entry Name is ${e.name} EntryID is ${e.id} MapToMyProfile is ${e.user_public_profile_userId} Value is ${e.myUserProfileId}" )
}
// Extract 2 All from original query
for(e: Entry in dao.getMyData()) {
Log.d("ENTRYINFO(2)","Entry Name is ${e.name} EntryID is ${e.id} MapToMyProfile is ${e.user_public_profile_userId} Value is ${e.myUserProfileId}" )
}
// Extract 3 getting useful data from the 2nd (JOINED) table
for(ep: EntryPlus in dao.getMyOtherData()) {
Log.d("ENTRYINFO(3)",
"Entry Name is ${ep.entry.name} EntryID is ${ep.entry.id} MapToMyProfile is ${ep.entry.user_public_profile_userId} Myuserprofile(From Entry) ${ep.entry.myUserProfileId}" +
" MyProfileId (From MyProfile) is ${ep.profileIdFromMyProfile} OtherData (From MyProfile) is ${ep.otherdataFromMyProfile}" )
}
The output to the Log is :-
2021-07-07 09:44:12.665 D/ENTRYINFO(1): Entry Name is Entry1 EntryID is 100 MapToMyProfile is 1 Value is 0
2021-07-07 09:44:12.665 D/ENTRYINFO(1): Entry Name is Entry2 EntryID is 200 MapToMyProfile is 1 Value is 0
2021-07-07 09:44:12.666 D/ENTRYINFO(2): Entry Name is Entry1 EntryID is 100 MapToMyProfile is 1 Value is 1
2021-07-07 09:44:12.666 D/ENTRYINFO(2): Entry Name is Entry2 EntryID is 200 MapToMyProfile is 1 Value is 1
2021-07-07 09:44:12.667 D/ENTRYINFO(3): Entry Name is Entry1 EntryID is 100 MapToMyProfile is 1 Myuserprofile(From Entry) 0 MyProfileId (From MyProfile) is 1 OtherData (From MyProfile) is myprofile1
2021-07-07 09:44:12.668 D/ENTRYINFO(3): Entry Name is Entry2 EntryID is 200 MapToMyProfile is 1 Myuserprofile(From Entry) 0 MyProfileId (From MyProfile) is 1 OtherData (From MyProfile) is myprofile1
Notes on the Output
The first two lines note that the MyProfileId value (i.e. Value =) is 0 as was inserted.
The Second two lines, using your original query shows that MyProfileId (Value =) is now the same value as the link/reference/association/relationship (i.e. the user_public_profile_userId column) to the MyProfile row.
The Third shows that the values from the MyProfile table (the otherdata column) have been extracted.
However, you are also appearing to describe the nature of LEFT JOIN in comparison to just JOIN.
If the following line (another Entry row but referencing a non-existent MyProfile row) were added before the extract:-
dao.insert(Entry(300,"Entry3",999,10 /* 10 = No such myprofile row */))
then the changes in the result will be significant in that the 2nd extract retrieves null for the my_profile_id as per :-
D/ENTRYINFO(2): Entry Name is Entry3 EntryID is 300 MapToMyProfile is 10 Value is null
changing to just JOIN (not LEFT JOIN) and the row which references the non-existent MyProfile is omitted. This is the documented impact as per :-
If the join-operator is a "LEFT JOIN" or "LEFT OUTER JOIN", then after the ON or USING filtering clauses have been applied, an extra row is added to the output for each row in the original left-hand input dataset that corresponds to no rows at all in the composite dataset (if any). The added rows contain NULL values in the columns that would normally contain values copied from the right-hand input dataset.
SQLite SELECT
In the case of null values, if this is your issue, then you need to decide what to do.
You could for example use the SQLite COALESCE function to change the null. e.g. SELECT entries.*, coalesce(myProfile.myProfileId,9999) as my_profile_id FROM Entry ....
Perhaps you need to ensure that the referential integrity is maintained, in which case you can utilise FOREIGN KEYS to enforce referential integrity. Perhaps refer to Foreign Key

How to check if record exists or not using Anko?

I am learning to fetching data from sqlite using anko. I can print the data successfully (if the record exist) but my application always crash when the data doesn't exist.
the error says:
parseSingle accepts only cursors with a single entry
I know exactly the meaning of error, I just dont know how to solve it.
here is the code for query:
fun getUserByUid(uid: Int): UserModel
{
val data = context.database.use {
val db = context.database.readableDatabase
val columns = UserModel.COLUMN_ID + "," + UserModel.COLUMN_NAME + "," + UserModel.COLUMN_API_KEY
val query = db.select(UserModel.TABLE_NAME, columns)
.whereArgs("(uid = {userId})",
"userId" to uid)
query.exec {
val rowParser = classParser<UserModel>()
parseSingle(rowParser) // this line that trigger error exception
}
}
return data
}
I tried to find count function in query or rowParser variable to check if the record exist or not but could not find it.
From the wiki page.
https://github.com/Kotlin/anko/wiki/Anko-SQLite#parsing-query-results
Parsing query results
So we have some Cursor, and how can we parse it into regular classes? Anko provides functions parseSingle, parseOpt and parseList to do it much more easily.
Method Description
parseSingle(rowParser): T Parse exactly one row
parseOpt(rowParser): T? Parse zero or one row
parseList(rowParser): List Parse zero or more rows
Note that parseSingle() and parseOpt() will throw an exception if the received Cursor contains more than one row.

ORMLite alias in rawQuery

Is it possible to use an alias (AS) in a query for ORMLite in Android? I am trying to use it with the following code:
String query =
"SELECT *, (duration - elapsed) AS remaining FROM KitchenTimer ORDER BY remaining";
GenericRawResults<KitchenTimer> rawResults =
getHelper().getKitchenTimerDao().queryRaw(
query, getHelper().getKitchenTimerDao().getRawRowMapper());
But when this codes gets executed it gives the following error:
java.lang.IllegalArgumentException: Unknown column name 'remaining' in table kitchentimer
java.lang.IllegalArgumentException: Unknown column name 'remaining' in table kitchentimer
The raw-row-mapper associated with your KitchenTimerDao expects the results to correspond directly with the KitchenTimer entity columns. However, since you are adding your remaining column, it doesn't no where to put that result column, hence the exception. This is a raw-query so you will need to come up with your own results mapper -- you can't use the DAO's. See the docs on raw queries.
For instance, if you want to map the results into your own object Foo then you could do something like:
String query =
"SELECT *, (duration - elapsed) AS remaining FROM KitchenTimer ORDER BY remaining";
GenericRawResults<Foo> rawResults =
orderDao.queryRaw(query, new RawRowMapper<Foo>() {
public Foo mapRow(String[] columnNames, String[] resultColumns) {
// assuming 0th field is the * and 1st field is remaining
return new Foo(resultColumns[0], Integer.parseInt(resultColumns[1]));
}
});
// page through the results
for (Foo foo : rawResults) {
System.out.println("Name " + foo.name + " has " + foo.remaining + " remaining seconds");
}
rawResults.close();
I had the same problem. I wanted to get a list of objects but adding a new attribute with an alias.
To continue using the object mapper from OrmLite I used a RawRowMapper to receive columns and results. But instead of convert all columns manually I read the alias first and remove its reference in the column arrays. Then it is possible to use the OrmLite Dao mapper.
I write it in Kotlin code:
val rawResults = dao.queryRaw<Foo>(sql, RawRowMapper { columnNames, resultColumns ->
// convert array to list
val listNames = columnNames.toMutableList()
val listResults = resultColumns.toMutableList()
// get the index of the column not included in dao
val index = listNames.indexOf(ALIAS)
if (index == -1) {
// There is an error in the request because Alias was not received
return#RawRowMapper Foo()
}
// save the result
val aliasValue = listResults[index]
// remove the name and column
listNames.removeAt(index)
listResults.removeAt(index)
// map row
val foo = dao.rawRowMapper.mapRow(
listNames.toTypedArray(),
listResults.toTypedArray()
) as Foo
// add alias value. In my case I save it in the same object
// but another way is to create outside of mapping a list and
// add this value in the list if you don't want value and object together
foo.aliasValue = aliasValue
// return the generated object
return#RawRowMapper foo
})
It is not the shortest solution but for me it is very important to keep using the same mappers. It avoid errors when an attribute is added to a table and you don't remember to update the mapping.

Categories

Resources