Sqlite: SqliteDatabase.delete() vs a raw query - android

Conclusion: Android's database APIs work but the documentation is horribly incomplete.
I have recently run into a brain wrecking situation due to the flexibility Sqlite provides by not forcing you to specify the data type when creating the table. The problem was my mindset that assumed that every data type would be a general character sequence if not specified and therefore the way to talk to database is through java.lang.String.
But you can't blame me either when you see methods like the below:
int delete (String table,
String whereClause,
String[] whereArgs)
in the SqlDatabase class from Android docs.
I have a table consisting of Phone No(that I stored as java.lang.String) and Timestamp as a long field. When I tried deleting a record using this method, it just never got deleted despite countless debugging.
I checked everything and query was alright and table is existent and all the checklist until by chance, I discovered that removing the '' around the timestamp while querying in a raw manner instead of using the above method yields a successful deletion, something like this:
DELETE FROM messages_records_table WHERE messageTimestamp = 1508494606000;
instead of the following:
DELETE FROM messages_records_table WHERE messageTimestamp = '1508494606000';
or,
DELETE FROM messages_records_table WHERE messageTimestamp = "1508494606000";
Phone No isn't a problem; it's the timestamp that was creating the problem in INSERTION/DELETION
So, I tried running a raw deletion query with quotes removed(that are required with a string/varchar type) and it yielded successful deletion. I used the following method for this:
db.execSQL(String sql, Object[] whereArgs)
The key thing to notice here is that Object[] is different from String[] when compared to delete(). I passed a Long to Object to make it work but passing a Long.toString() in delete() seems to be useless.
So my question is, Is my analysis correct and delete() API is basically useless or have I missed some bigger picture..after all, it's provided by Android team carefully?

SQLite supports multiple data types; and while column types are not strictly enforced, values might be automatically converted in some cases (this is called affinity).
When your values are stored as numbers, you should access them as numbers, not as strings.
The Android database API does not allow you to use parameter types other than strings in most functions. This is a horrible design bug.
To search for a number, either use execSQL(), which allows you to use number parameters, or convert the string value back into a number:
db.delete(..., "timestamp = CAST(? AS NUMBER)",
new String[]{ String.valueOf(ts) });

The problem was my mindset that assumed that every data type would be
a general character sequence if not specified and therefore the way to
talk to database is through java.lang.String.
I think that's the real issue.
If you specify no type e.g.
CREATE TABLE mytable (col1,col2,col3)
Then according to Determination of Column Affinity(3.1) rule 3:-
3) If the declared type for a column contains the string "BLOB" or if no
type is specified then the column has affinity BLOB.
And then according to Section 3
A column with affinity BLOB does not prefer one storage class over
another and no attempt is made to coerce data from one storage class
into another.
I've personally never had an issue with delete. However I do have a tendency to always delete according to rowid.
Here's a working example usage that shows that delete isn't useless and is deleting according to a long. However the columns are all of type INTEGER :-
int pudeletes;
int sldeletes;
int rdeletes;
int pdeletes;
if(doesProductExist(productid)) {
// if not in a transaction then begin a transaction
if(!intransaction) {
db.beginTransaction();
}
String whereargs[] = { Long.toString(productid)};
// Delete ProductUsage rows that use this product
pudeletes = db.delete(
DBProductusageTableConstants.PRODUCTUSAGE_TABLE,
DBProductusageTableConstants.PRODUCTUSAGE_PRODUCTREF_COL +
" = ?",
whereargs
);
// Delete ShopList rows that use this product
sldeletes = db.delete(
DBShopListTableConstants.SHOPLIST_TABLE,
DBShopListTableConstants.SHOPLIST_PRODUCTREF_COL +
" = ?",
whereargs
);
// Delete Rules rows that use this product
rdeletes = db.delete(
DBRulesTableConstants.RULES_TABLE,
DBRulesTableConstants.RULES_PRODUCTREF_COL +
" = ?",
whereargs
);
// Delete the Product
pdeletes = db.delete(
DBProductsTableConstants.PRODUCTS_TABLE,
DBProductsTableConstants.PRODUCTS_ID_COL +
" = ?",
whereargs
);
// if originally not in a transaction then as one was started
// complete and end the transaction
if(!intransaction) {
db.setTransactionSuccessful();
db.endTransaction();
}
}

Related

How to fix empty string while fetching it from Database?

I am fetching a string from Database using the column id. When I enter a query in SQLite DB Browser it returns what is need but the same query returns nothing when coded through Java.
My Data Base contains a table named drugs which has 3 columns i.e. drug_id, drug_name and drug_overview. Using drug_id i am fetching drug_overview. I have tried the query in db browser which returns me the correct string from drug_overview but the same query returns nothing when coded through java.
SQLite DB Browser query:
SELECT * FROM drugs Where drug_id = 50;
JAVA CODE:
String query105 = "SELECT * FROM drugs Where drug_id = " + drug_id;
Log.e("TESTDB1","Drugs table query: " + query105);
Cursor c105 = db.rawQuery(query105,null);
if (c105 != null){
while (c105.moveToNext()){
String overview = c105.getString(c105.getColumnIndexOrThrow("drug_overview"));
Log.e("TESTDB1","Overview: " + overview);
}
c105.close();
}
Expected result is Overview: Acyclovir is an antiviral drug. It slows the growth and spread of the herpes virus in the body. It will not cure herpes, but it can lessen the symptoms of the infection.Acyclovir is used to treat infections caused by herpes viruses, such as genital herpes, cold sores, shingles, and chicken pox, as well as varicella (chickenpox), and cytomegalovirus.Acyclovir may also be used for purposes not listed in this medication guide.
But the actual result is Overview:
empty
. When i change the id in my query it gives the correct result from a different drug.
I am afraid that your problem may be with the actual data itself as Mike said in comment, I think your database in the files is old and you haven't copied the latest to folder. Try to re-install and delete old database
Your query returns 0 or 1 lines so I think you should use c105.moveToFirst() instead of c105.moveToNext(). moveToNext is supposed to be used for a list, not for a single entry. Do something like:
if (c105.moveToFirst()){
String overview = c105.getString(c105.getColumnIndex("drug_overview"));
// do something with the result
}
c105.close();

Is it better to use Update or execSQL to update data on SQLite?

I saw in a tutorial a code updating data from a SQlite database using execSQL:
String update = "UPDATE FRUIT SET COLOR=? WHERE ID=?";
myDatabase.execSQL( update, new Object[] {"RED", 7});
Cursor cursor = myDatabase.rawQuery("SELECT * FROM FRUIT;", null);
if (cursor.moveToFirst()) {
do {
String name = cursor.getString(cursor.getColumnIndex("NAME"));
String color = cursor.getString(cursor.getColumnIndex("COLOR"));
Log.i(TAG, "onCreate: Name: " + name + ", color: " + color);
} while (cursor.moveToNext());
}
but, I read this in the oficial documentation of Android:
The code using execSQL worked but it's better to use update or I can still use execSQL since it worked? What's better for good practice? Since this tutorial is from a trustworthy source, why are they using execSQL?
The issue/warning regarding using execSQL may be for a few reasons, one of them being that you do no get anything returned when using execSQL, whilst the convenience methods return potentially useful values.
insert will return a long containing the id of the inserted row else -1.
update and delete return the number of affected rows.
Using the convenience methods also reduces the chance of making typing errors by build the underlying SQL, adding the keywords and enclosing strings and for some importantly offering a level of protection against SQL injection (execSQL's
bindArgs also offers protection against SQL Injection, likewise with rawQuery).
However, there are limitations and sometimes the use of execSQL/rawQuery becomes necessary for some more complex situations.

SQLite query in Android Nougat is not backwards compatible

I have an SQLite query that does not return a record by _id on Android Nougat but does return a record on Android pre-Nougat.
When I attach a debugger to the Nougat emulator and start poking around, I observe the following:
Original Code
readableDatabase.query("report_view", null, "_id = ?", new String[]{"2016309001"}, null, null, null).getCount()
Result: 0
Poking around / Evaluate expression
readableDatabase.rawQuery("SELECT * FROM report_view WHERE _id = 2016309001", null).getCount()
Result: 1
Enabling the SQLite log results in (note the single quotes):
V/SQLiteStatements: /data/user/0/xyz/bla.db: "SELECT * FROM report_view WHERE _id = '2016309001'"
For the non-raw query on Nougat. I cannot seem to enable the SQLite log on the pre-Nougat (Marshmallow) device.
So it seems the issue is caused by Android surrounding the parameter with single quotes. The _id column is of type integer. And a value surrounded by quotes is of type string. Why is this suddenly an issue on Nougat?
Unless I am mistaken your original code passes the date parameter explicitly as a string (new String[]), and the Android documentation for the query method, selectioArgs parameter states
selectionArgs String: You may include ?s in selection, which will be
replaced by the values from selectionArgs, in order that they appear
in the selection. The values will be bound as Strings.
So if you want something else than strings, you should probably perform a cast, or better, do not use the query and rawQuery methods, but prepared statements (SQLiteStatement), which will allow you to specify the type of parameters.
Also the '?' being substituted in the log should be a "fake" substitution for logging purposes (at least I hope it is a fake substitution for logging purposes, as Android should be using a bound parameter there, and not replace it in the query).
Finally the field types in Android are used only for type affinity, so it is entirely possible that even if you declared your field as "integer" in the table, if you used the Query method and its String-only parameters, you actually ended up with strings in your records.
In theory, SQLite should not behave differently. It is possible that you are using a different table definition that results in a different affinity.
Anyway, the parameter you give to the query function is a string, so it is not surprising that the database handles it as a string.
If you want to give a number to the database, you have to make the parameter a number.
And the Android framework doesn't allow this for most database functions, so you have to put it directly into the string:
db.query("report_view", null, "_id = " + "2016309001", null, null, null, null).getCount();
Please note that there is a helper function for counting rows:
DatabaseUtils.queryNumEntries(db, "report_view", "_id = " + "2016309001");

GreenDAO on Android: Byte Array as Primary Key / Building Query Containing Where Clause on Byte Array Property

I am currently trying to select entries using a query containing a where condition on a (byte array) property. This property/column contains a serialized UUID. Unfortunately I currently cannot change the data type of this column, as the database is created and synced by a separate module which only works properly with the current implementation. As greenDao cannot handle byte arrays as primary keys properly I am trying to work around this issue somehow. Creating my own selection queries etc. would be a solution
The property is defined during greenDAO generation as:
Entity randomEntity = schema.addEntity("RandomEntity");
...
randomEntity.addByteArrayProperty("RandomProperty");
The query is built using following line:
Query query = this.mRandomEntityDao.queryBuilder().where(RandomEntityDao.Properties.RandomProperty.eq(randomByteArray)).build();
Unfortunately I get following error with this operation:
de.greenrobot.dao.DaoException: Illegal value: found array, but simple object required
at de.greenrobot.dao.query.WhereCondition$PropertyCondition.checkValueForType(WhereCondition.java:75)
...
Is this where condition is simply not supported by greenDAO or am I missing something crucial? Unfortunately I cannot use another datatype for this certain property.
Edit:
My current workaround goes as follows:
As greenDao can handle primary keys which are strings (up to a certain point) and UUIDs can also be represented by this data type I altered the existing tables and added following columns:
db.execSQL("ALTER TABLE 'RANDOMTABLE' ADD COLUMN '_GREENID' TEXT;");
The sync module is ignoring this column so there shouldn’t be any issues with that. Then I created a trigger mapping the serialized UUID in the ID column to the new _GREENID column:
db.execSQL("CREATE TRIGGER randomtableGreen AFTER INSERT ON 'RANDOMTABLE' BEGIN " +
"UPDATE 'RANDOMTABLE' SET '_GREENID' = HEX(NEW.ID) WHERE 'RANDOMTABLE'.ID = NEW.ID; " +
"END;");
Finally I run an update on the table in case they already contained some entries prior to the creation of the trigger:
db.execSQL("UPDATE 'RANDOMTABLE' SET '_GREENID' = HEX(ID) WHERE '_GREENID' <> '';");
About byte[] aka BLOB in greendao:
Looking at de.greenrobot.dao.query.WhereCondition.PropertyCondition.checkValueForType conditions for byte[] are not supported at the moment, because the following lines will always throw an exception if value is of type byte[].
if (value != null && value.getClass().isArray()) {
throw new DaoException("Illegal value: found array, but simple object required");
}
Solution 1 - modify and contribute to greendao:
You could modify the uper lines, so that the exception is only thrown if the type of value and the type of the Property don't fit.
if (value != null) {
if (value.getClass().isArray() && !property.type.isArray()) {
throw new DaoException("Illegal value: found array, but " +
"simple object required");
}
if (!value.getClass().isArray() && property.type.isArray()) {
throw new DaoException("Illegal value: found simple object, " +
"but array required");
}
}
Maybe this will already solve the problem, but there may be other parts in greendao stopping to work with this edit or that will break the query. For example the binding of parameters to queries may not work with arrays.
Solutinon 2 - Use queryRaw(String where, String... selectionArg)
This is pretty straight forward and shouldn't be a problem with some knowledge about SQLite.
Solution 3 - Using a lookup-table
Assume the original table:
ORIG
-------------------------------
UUID BLOB
...
You can modify ORIG and add a autoincrement-primarykey:
db.execSQL("ALTER TABLE 'ORIG' " +
"ADD COLUMN 'REF_ID' INT PRIMARYKEY AUTOINCREMENT;");
The sync service should already take care about the uniqueness of ORIG.UUID and ignore the new ORIG.REF_ID-column. For inserting new UUIDs the sync service will probably use INSERT causing a new autoincremented value in ORIG.REF_ID.
For updating an existing UUID the sync service will probably use UPDATE ... WHERE UUID=? and no new ORIG.REF_ID-value will be created, but the old value will be kept.
Summarized the ORIG-table has a new Bijection between column REF_ID and column UUID.
Now you can create another table:
ORIG_IDX
------------------------------
UUID TEXT PRIMARYKEY
REF_ID INT UNIQUE
(If your data is smaller than 8 bytes it will also fit into a INT instead TEXT, but I don't know if there is a built-in cast/coversion from BLOB to INT.)
ORIG.IDX.UUID will be the String-representation of ORIG.UUID.
ORIG_IDX.REF_ID is foreign key for ORIG.REF_ID.
ORIG_IDX is filled and updated by triggers:
db.execSQL("CREATE TRIGGER T_ORIG_AI AFTER INSERT ON 'ORIG' BEGIN " +
"INSERT 'ORIG_IDX' SET 'REF_ID' = NEW.REF_ID, 'UUID' = NEW.UUID" +
"END;");
Create corresponding triggers for UPDATE and DELETE.
You can create the tables ORIG and ORIG_IDX using greendao and then query a requested uuid with:
public Orig getOrig(String uuid) {
OrigIdx origIdx = OrigIdxDao.queryBuilder().where(
QrigIdxDao.Properties.UUID.eq(uuid)).unique();
if (origIdx != null) {
return origIdx.getOrig();
}
return null;
}
I think String-primarykey is not supported yet, so dao.load(uuid) won't be available.
CONCERING AN EXTENDED TABLE:
You could use a string primarykey-column and provide conversion-methods in the keep-sections of your entity. You will have to compute the primarykey-column before you do an insert.
If there are other tools inserting data (for example your sync service) you'd have to use a trigger to compute your primary-key before the insert happens. This doesn't seem possible using SQLite. Thus the primarykey-constraint will fail on inserts by the sync service, so this solution will not work with primarykey!

Android - passing field name as selectionArgs to rawQuery

I'm passing the below statement as a rawQuery in Android:
SELECT DISTINCT ltUsers._id,ltUsers.NAME,ltUsers.GLOBAL_ID, ltGroups.GROUP_NAME
FROM ltUsers
JOIN ltGroups ON (ltUsers.GROUP_ID = ltGroups.GLOBAL_ID)
WHERE ltgroups.GLOBAL_ID = ? " +
ORDER BY ltUsers.NAME ASC,ltgroups.GLOBAL_ID ASC;
With the rawQuery as follows:
Cursor c = db.rawQuery(sql,args)
It works just fine if I pass a value to the parameter, e.g.
String[] args = new String[]{"2"}
However, I also want to be able to show all rows, unlimited by the GLOBAL_ID in the WHERE clause. Testing on a dump of my SQLite database outside of Android - as well as in Android by just writing the parameter directly into the statement - shows the following clause to be a valid way to do this:
WHERE ltGroups.GLOBAL_ID = ltGroups.GLOBAL_ID
Yet when I pass the field reference ltGroups.GLOBAL_ID or [ltGroups].[GLOBAL_ID] as a parameter it fails to return any rows in the rawQuery. Any ideas on why this might be happening? Happy to produce any extra information.
Parameters always replace specific values, not anything else.
When you put the string "ltGroups.GLOBAL_ID" into the parameters array, it is interpreted as exactly that, a string.
(To show all records, just omit the WHERE clause.)

Categories

Resources