I have a read-only database connection. Sometimes, when reading data from the database with a SELECT query, it throws a SQLiteReadOnlyDatabaseException.
I open the connection like this:
return SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY);
The query is:
Select * FROM BudgetVersions WHERE entityId = ?
I read data from the database using db.rawQuery(), like this:
String query = ...;
Cursor c = db.rawQuery(query, new String[]{ activeBudgetId });
try {
if (c.moveToFirst()) {
bv.versionName = c.getString(c.getColumnIndexOrThrow("versionName"));
return bv;
} else {
return null;
}
} finally {
c.close();
}
Very rarely, I get a crash like this, inside the call to c.moveToFirst():
Caused by: android.database.sqlite.SQLiteReadOnlyDatabaseException: attempt to write a readonly database (code 776)
at android.database.sqlite.SQLiteConnection.nativeExecuteForCursorWindow(Native Method)
at android.database.sqlite.SQLiteConnection.executeForCursorWindow(SQLiteConnection.java:845)
at android.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:836)
at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62)
at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:144)
at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:133)
at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:197)
at android.database.AbstractCursor.moveToFirst(AbstractCursor.java:237)
As a workaround, I could try using a writable database connection instead, but I'd like to know why the crash is happening.
The table I'm reading from is a standard SQLite table:
CREATE TABLE BudgetVersions (
entityId VARCHAR PRIMARY KEY NOT NULL UNIQUE,
budgetId VARCHAR NOT NULL,
versionName VARCHAR NOT NULL,
dateFormat VARCHAR,
currencyFormat VARCHAR,
lastAccessedOn DATETIME,
isTombstone BOOL NOT NULL,
deviceKnowledge NUMERIC NOT NULL
);
I've seen the crash happen on both a KitKat emulator and a device running Lollipop.
There is a separate writeable connection open to the same database at the same time, owned by a WebView. The database is being updated by Javascript code in the WebView, and read from in the native Android/Java layer with this read-only connection.
I expect this may prove to be the ultimate cause of the problem, but I'd like to understand in detail why a read-only connection would interfere with a separate writeable connection.
I am well aware that the general advice is to use a single connection to the database, but since the writeable connection is owned by the WebView, I don't have easy access to it from the Java code.
Solved by changing it to a writeable database connection. The clue was in the documentation for the 776 error code:
(776) SQLITE_READONLY_ROLLBACK
The SQLITE_READONLY_ROLLBACK error code is an extended error code for
SQLITE_READONLY. The SQLITE_READONLY_ROLLBACK error code indicates
that a database cannot be opened because it has a hot journal that
needs to be rolled back but cannot because the database is readonly.
During development, I am frequently interrupting the currently-running app to install and run a new version. This causes the currently-running app to be force-stopped by the system. If the Javascript code in the WebView is in the middle of writing to the database via its separate writeable connection when the app is nuked, then a hot journal will be left behind.
When the new version of the app starts up, the read-only database connection in the native Java code is opened. When this connection spots the journal, it tries to roll back the journal. And because it's a read-only connection, it fails.
(This fits with the crash being observed immediately on startup after I've made a change.)
The correct fix is therefore to make the Java connection a writeable connection. This connection never attempts a write during normal operation, but it must write when recovering from a previous interrupted write through the WebView's writeable connection.
Related
I am working on an app which saves transactions in SQLite database after payment is received.
As soon as the transaction is approved, it is inserted in database and after that user is shown the approval.
I show the approval when I get the result from
sqlitedatabase.insert() method, and check that it returns row number.
public static boolean insert(String tableName, SQLiteDatabase sqLiteDatabase, ContentValues contentValues){
long rowId = sqLiteDatabase.insert(tableName, null, contentValues);
return rowId != -1;
}
The problem is, when the approval is shown to user, I shut down device by ejecting battery (I am testing out of battery case) and
when I reboot device, I see that no record is inserted in the database. That makes me think that SQLite is inserting records asynchronously after returning the row id.
I am using AsyncTask and I do inserting in the doInBackground, and I show the user approval in onPostExecute() of AsyncTask.
My question is, is there any way to make sure that the record is inserted the moment I call insert method or am I doing something wrong here?
After some research, I realized what was causing the problem.
It was SQLite's default configuration setting and synchronous setting was not FULL. According to documentation:
FULL (2) When synchronous is FULL (2), the SQLite database engine will
use the xSync method of the VFS to ensure that all content is safely
written to the disk surface prior to continuing. This ensures that an
operating system crash or power failure will not corrupt the database.
FULL synchronous is very safe, but it is also slower. FULL is the most
commonly used synchronous setting when not in WAL mode.
The default configuration was not FULL and I changed it in my SQLiteOpenHelper class' onConfigure callback. And it solved the problem.
#Override
public void onConfigure(SQLiteDatabase db) {
db.execSQL("PRAGMA synchronous = 2");
}
Although the document states that "FULL synchronous is very safe, but it is also slower.", I haven't seen a significant performance difference despite batch inserting in some cases.
Firstly, I create a database called "mydb" in my Android app:
DBHelper dbHelper = new DBHelper(context, "mydb", null, 1);//DBHelper is my custom class
And write some data into it's table:
SQLiteDatabase db = dbHelper.getReadableDatabase();
db.execSQL("insert into mytable(name, text) values ('allen','hello')");
Here, everything is ok. But then, i delete this database manually not by programming, with a software "R.E. explore" (Certainly on a rooted device).
Then, in my code, i read this table of the database. What is astonishing is that i still could get the data I stored.
Cursor cursor = db.query("mytable", new String[]{"name","text"}, null, null, null, null, null);
Why?
Quoting from the Android Developers reference website:
Once opened successfully, the database is cached, so you can call
this method every time you need to write to the database. (Make sure
to call close() when you no longer need the database.)
This is from the description of the getWritableDatabase() method, however both getReadableDatabase() and getWritableDatabase() return basically the same object for reading the database.
Please note that you should use getWritableDatabase() if you want to persist the changes you make to the database on the device's internal memory. Otherwise they will be valid only for the duration of the application's runtime and will be discarded once the app is closed. If you wish to delete the database completely, you should call the SQLiteDatabase's close() method in order to invalidate the cache.
use SQLiteDatabase.deleteDatabase(File file) API to delete the database
Deletes a database including its journal file and other auxiliary files that may have been created by the database engine.
Make sure you have closed all the connections that are open.
In case you are not able to do that,
just cal the deleteDatabase followed by kill process.. - not recommended
You need to delete the app from your phone then install again
Can anybody explain me why following happens. This is Android app which is using SQLite db.
App adding new data to db, which stored in external memory.
At the same time, this external memory becomes unavailable (for example, I mount it to PC).
When external memory becomes avaliable again, I can't access db in read_only mode. But, if I try to open it in read-write mode, it goes well. Why?
I wrote code (below), which handle this problem and it works well, but I still don't understand why.
This is NOT related to remaining open connections or stuff like this. To insure, I totally removed my app from device and re-install it. But even after that, db was unavailable in read-only mode. After db opened once in read-write mode, it may be open in read-only next time without any problems.
When I say "unavailable", I mean that db opens well, but any query to db failed with IO error.
SQLiteDatabase db = SQLiteDatabase.openDatabase(f.getPath(), null, SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
if (!testQuery(db))
{
Utils.logD("Failed to open db at path: " + f + ". Trying recovering...");
db.close();
db = SQLiteDatabase.openDatabase(f.getPath(), null, SQLiteDatabase.NO_LOCALIZED_COLLATORS);
if (!testQuery(db))
{
Utils.logD("Failed to open db at path: " + f + " (noway)");
db.close();
return null;
}
}
Utils.logD("DATABASE OPENED! " + db.hashCode());
If the DB was made unavailable while a transaction was still active, this transaction must be rolled back before the DB is again in a consistent state. This rollback requires write access.
This implies that you should open a DB in readonly mode only if it gets never ever changed.
I'm getting a weird error in DDMS, it appears just after the innocent "Finalizing a Cursor that has not been deactivated or closed", comming from my own DataAdapter (about that in next question)
Error:
"Finalizing a Cursor that has not been deactivated or closed. database = /data/data/net.toload.main/databases/lime, table = null, query = SELECT _id, code, code3r, word, score FROM mapping WHERE code3r = '0' AND code ='HTT' ORDER BY cod"
I didn't create any tables with such columns in my app! And my localdatabase isn't stored there... /databases/lime, but this error seems to come just after my real error from my own DataAdapter. I tried to pull the lime.db file to read the contents of the database but the pulled file is always of 0 bytes, though on DDMS-FileExplorer is much more, now 26624bytes.
So ... im thinking that maybe android/google is tracking my every move!! and the app doesnt run in a isolated process...
Anyway can you explain this error?
Your project must be using this Lime IME (Lightweight Input Method Editor):
http://code.google.com/p/limeime/
I have an app that uses a database with 3 tables in it. Those 3 tables have data read from and written to them by activities and services.
Having gotten a few "android.database.sqlite.SQLiteException: database is locked" crashes, I went in to the database adapter class and wrapped every write, update, or delete function with a synchronized statement, so like:
public int deleteExpiredAlarms() {
String whereClause = FIELD_EXPIRED + " = 1";
int val = 0;
synchronized(dbWriteLock) {
val = db.delete(ALARM_DATABASE_TABLE, whereClause, null);
}
return val;
}
That seemed to make it better. But lately it's gotten bad again as I've added more services that read and write to different tables.
Do I need to synchronize ALL db access statements, including queries?
The exception is occurring on the attempt to open the writable database via the open helper...should I synchronize that act also?
I've heard that I should only be using one db helper so that there won't be issues with multiple threads accessing the db. How do I use only one db helper? Every example I've seen so far has the db helper as an instantiated value inside the db adapter....so wouldn't that be a separate db helper per db adapter instantiated (one in an activity, one in a service running,etc)
I've looked at using a content provider instead, as it's been claimed to solve problems like this, but it's really more work than I want to do if I should be able to have direct db access without locking issues. And I do not plan to make this db accessible to other apps.
Thanks for the help.