0

I have a content provider backed by an SQLiteDatabase. Consider the following two calls:

//the following call results in "DELETE FROM DETAILS":
context.getContentResolver().delete(getContentUriOfDetailTable(), null, null);   //line 1
//the following call results in "DELETE FROM MASTER": 
context.getContentResolver().delete(getContentUriOfMasterTable(), null, null);   //line 2

As I know content providers are thread safe, when backed by an SQLiteDatabase. But is there a guarantee, that the execution of the SQL triggered by line 1 is finished before the SQL triggered by line 2 will be executed?

Background: There is a foreign key reference from table DETAIL to MASTER and I got a bugreport

SQLiteConstraintException: foreign key constraint failed (code 19)

from line 2, though I cannot reproduce it.

Tables:

CREATE TABLE MASTER 
(_id PRIMARY KEY autoincrement,
VALUE text
);

CREATE TABLE DETAILS (
_id PRIMARY KEY autoincrement,
MASTERIDX integer not null,
VALUE text,
FOREIGN KEY(MASTERIDX) REFERENCES MASTER(_id)
);

My ContentProvider is nothing special:

public class MyDbContentProvider extends ContentProvider {

    private MyDbOpenHelper mDbHelper;

    //...
    //...

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        SQLiteDatabase db = mDbHelper.getWritableDatabase();
        int rowsDeleted = 0;
        String where;

        MyUriMatcher.MatchResult matchRes = mURIMatcher.matchTable(uri);
        TableDef tableDef = matchRes.tableDef;

        switch (matchRes.uriType) {
        case ALL_ITEMS:
            where = selection;
            break;
        case SINGLE_ITEM:
            String idstr = uri.getLastPathSegment();
            where = TableDef._ID + " = " + idstr;
            if (!TextUtils.isEmpty(selection)) {
                where += " AND (" + selection + ")";
            }
            break;
        default:
            throw new IllegalArgumentException("Unsupported URI for delete: " + uri);
        }

        try {
            rowsDeleted = db.delete(tableDef.getTableName(), where, selectionArgs);
        } catch (SQLException e) {
            throw createCustomSqlException(e, "DELETE", uri, null, where, selectionArgs);
        }       
        // notify all listeners of changes:
        if (rowsDeleted > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return rowsDeleted;
    }
}

Here is the stack trace:

android.database.SQLException: SQLiteConstraintException: foreign key constraint failed (code 19) for DELETE for following URI: content://de.myapp.mobil.myappDbProvider/master and this selection: null
at android.database.sqlite.SQLiteConnection.nativeExecuteForChangedRowCount(Native Method)
at android.database.sqlite.SQLiteConnection.executeForChangedRowCount(SQLiteConnection.java:858)
at android.database.sqlite.SQLiteSession.executeForChangedRowCount(SQLiteSession.java:754)
at android.database.sqlite.SQLiteStatement.executeUpdateDelete(SQLiteStatement.java:64)
at android.database.sqlite.SQLiteDatabase.delete(SQLiteDatabase.java:1494)
at de.myapp.mobil.MyDbContentProvider.delete(MyDbContentProvider.java:267)
at android.content.ContentProvider$Transport.delete(ContentProvider.java:228)
at android.content.ContentResolver.delete(ContentResolver.java:958)
at de.myapp.CommonDbContract$TableDef.deleteAll(CommonDbContract.java:119)
at de.myapp.mobil.MyDbContract.deleteAll(MyDbContract.java:1730)
at de.myapp.mobil.MyDbContract.recreateDbOrDeleteAll(MyDbContract.java:1761)
at de.myapp.mobil.SettingsActivity$ResetAllCommand.execute(SettingsActivity.java:77)
at de.myapp.mobil.SettingsActivity$ResetAllCommand.execute(SettingsActivity.java:1)
at de.myapp.DlgUtils$DataCommand.execute(DlgUtils.java:54)
at de.myapp.DlgUtils$CombinedCommand.execute(DlgUtils.java:116)
at de.myapp.DlgUtils$CommandWrapper.onClick(DlgUtils.java:157)
nurealam11
  • 537
  • 4
  • 16
Alois Heimer
  • 1,772
  • 1
  • 18
  • 40
  • Are you sure there is not another app or thread accessing the provider simultaneously? It would be possible for another thread to make a call between your two calls above, modifying the MASTER table in a way which causes the failure at the call on line 2. – Larry Schiefer Jun 03 '14 at 11:13
  • Only one app is accessing this database. I cannot rule out however, that there does not exist another instance of the app, because the main activity has [launchMode singleTop instead of singleTask](http://stackoverflow.com/q/16693191/2306907). But I cannot imagine a practical situation where two instances modify this table because this is all user-driven. Hence you would have to switch screens with the speed of light, because there is no background process which adds data. – Alois Heimer Jun 03 '14 at 12:31
  • To answer your original question: yes, your two lines will run sequentially within your thread. Communication with the `ContentProvider` happens over blocking `Binder` calls behind the scenes. On the `ContentProvider` side, it *should* also execute sequentially. However, it would depend on the implementation. If it were doing something like spawning a thread to handle the delete it could be a cause for contention. That's probably unlikely, though. One thing which I'm not clear on: what foreign key(s) does your MASTER table have within it? – Larry Schiefer Jun 03 '14 at 12:39
  • @LarrySchiefer I do have control over the ContentProvider (see edited question). Do I understand you right, that if I do not implement the delete method using threads, all should be run sequentially? Do you have a reference for me? – Alois Heimer Jun 03 '14 at 13:15
  • Not exactly, no. Your provider's `delete` method may be entered multiple times simultaneously, if you have multiple threads/processes calling into your provider. My point was that if your app executing the 2 `delete()` methods were the only thing running, then they would execute sequentially both in your app and in the provider. – Larry Schiefer Jun 03 '14 at 13:21
  • Are you sure the exception isn't happening on the `delete()` call for the DETAIL table rather than the MASTER? In other words, what if the scenario is that this delete was run 1 time successfully, so DETAIL had an entry removed and so did MASTER. Then it was re-run with a new DETAIL id and that DETAIL entry had a foreign reference to the MASTER id which was removed the with the first call. – Larry Schiefer Jun 03 '14 at 13:24
  • @LarrySchiefer This was what I tried to say: If I do not implement the delete method of the content provider using threads internally (what I dont do, see edited question), then the methods will execute sequentially, under the assumption, that there is no other thread which modifies the database at the same time. This is because of the blocking `Binder` calls - do you have a reference for this? – Alois Heimer Jun 03 '14 at 13:42
  • @LarrySchiefer Regarding your other comment: I am sure, that the exception is happening for the MASTER table. I have added a trace to the original question. The content provider code results in "DELETE FROM DETAILS" and afterwards in "DELETE FROM MASTER" (it deletes everything = DB reset). For the other possibility to modify the DETAILS-table you have to leave the current activity, start two other activities and tap a button. So I cannot immagine this will happen simultaneously. – Alois Heimer Jun 03 '14 at 13:55
  • Check the AOSP sources: [ContentResolver](https://android.googlesource.com/platform/frameworks/base/+/android-4.4.2_r2/core/java/android/content/ContentResolver.java) and [IContentProvider](https://android.googlesource.com/platform/frameworks/base/+/android-4.4.2_r2/core/java/android/content/IContentProvider.java). The `IContentProvider` is a synchronous `Binder` interface, so all of the APIs defined within it are blocking. – Larry Schiefer Jun 03 '14 at 14:50
  • Check the SQLite documentation on foreign keys. If you attempt to delete all rows from the MASTER table, but there still exists a row in the DETAIL table which refers to a MASTER table ID then it will fail: http://www.sqlite.org/foreignkeys.html – Larry Schiefer Jun 03 '14 at 14:53
  • @LarrySchiefer I know about foreign keys. See above: the first call is `DELETE FROM DETAILS` (deletes all detail records), the second call is `DELETE FROM MASTER`. – Alois Heimer Jun 04 '14 at 09:25
  • @LarrySchiefer Thanks for your explanations. You should consider making an answer from your comments. I must admit, that I did not dig deep enough in the [code you linked to](https://android.googlesource.com/platform/frameworks/base/+/android-4.4.2_r2/core/java/android/content/ContentResolver.java) to really understand what is going on, but your comments are the best answer for my question so far. – Alois Heimer Jun 04 '14 at 09:25
  • Thanks, I'll copy it out to an answer. Glad it helped. – Larry Schiefer Jun 04 '14 at 12:34
  • Can you show us the MyUriMatcher class – danny117 Jun 07 '14 at 15:12
  • @danny117 What do you think, how can this matter? I already checked, that these two calls result in the SQL statements, which are mentioned in the comments. – Alois Heimer Jun 10 '14 at 10:41
  • @AloisHeimer Absolutely not. Actually I was admiring the technique where you pull out the table name. I have the table name hard coded based on the URI in my apps and was thinking that this could make things a bit more reusable. – danny117 Jun 10 '14 at 15:18
  • @danny117 All our table definitions inherit from TableDef. TableDef has abstract functions for table name, uri base name and for an id that is used to encode the URIs. That given the UriMatcher class is pretty straightforward. I think, I could post the code, but I think this would pollute this question. – Alois Heimer Jun 11 '14 at 13:20

2 Answers2

4

ContentResolver objects use Binder (IBinder) interfaces behind the scenes to communicate with ContentProvider instances. The Binder calls are blocking, so they will execute sequentially within your app. Check the AOSP sources: ContentResolver and IContentProvider. The IContentProvider is a synchronous Binder interface, there is no async keyword applied to the AIDL defined interface.

Also, check the SQLite documentation on foreign keys. If you attempt to delete all rows from the MASTER table, but there still exists a row in the DETAIL table which refers to a MASTER table ID then it will fail: sqlite.org/foreignkeys.html.

Larry Schiefer
  • 15,687
  • 2
  • 27
  • 33
  • Regarding the second paragraph: That's the only explanation for the error: There does still exist a row in the DETAIL table. If the two calls execute sequentially, there must be call from another process / thread which adds this row. – Alois Heimer Jun 10 '14 at 10:29
0

Instead of this

CREATE TABLE MASTER 
(_id PRIMARY KEY autoincrement,
VALUE text
);

CREATE TABLE DETAILS (
_id PRIMARY KEY autoincrement,
MASTERIDX integer not null,
VALUE text,
FOREIGN KEY(MASTERIDX) REFERENCES MASTER(_id)
);

Just update this

CREATE TABLE MASTER 
(_id PRIMARY KEY autoincrement,
VALUE text
);

CREATE TABLE DETAILS (
_id PRIMARY KEY autoincrement,
MASTERIDX integer not null,
VALUE text,
FOREIGN KEY(MASTERIDX) REFERENCES MASTER(_id) ON DELETE CASCADE
);

Now this line alone is enough to delete data in both master table and details table:

//the following call results in "DELETE FROM MASTER": 
context.getContentResolver().delete(getContentUriOfMasterTable(), null, null);   //line 2

For more info : http://www.sqlite.org/foreignkeys.html#fk_actions

Alois Heimer
  • 1,772
  • 1
  • 18
  • 40
Dinesh Raj
  • 664
  • 12
  • 30
  • I am aware of this possibility. But the original question remains. A possible non-sequential execution of this kind of calls would have a dramatic impact on my whole project. – Alois Heimer Jun 10 '14 at 10:35