4

I'm implementing a PRIVATE ContentProvider which has few tables with relationships (one to many, many to many). In my current implementation all of the tables are accessible by URIs. how can I simplify the interface so the inner 'through' tables won't have to be accessed by URIs ?

for example, I have a POSTS table, each POST has many TAGS through the TAGGINGS table. I want to interact only with the POSTS URI and do the 'private' work inside of the ContentProvider.

for query its simple to return a cursor with joined tables, but how do I do this with insert ? is bulkInsert what I should look into ?

Gal Ben-Haim
  • 17,433
  • 22
  • 78
  • 131

4 Answers4

7

It is a limitation of ContentProvider. If you are not exposing your data to other applications you can use your custom database adapter implementation with methods and queries straight hitting your requirements.

bulkInsert() won't help in this situation as it inserts rows only into one table at once. But take a look at ContentProvider.applyBatch() method and ContentProviderOperation, ContentProviderOperation.Builder classes (you may need withValueBackReference() for one-to-many inserting).

These links should help you understand how to use them:

http://www.grokkingandroid.com/better-performance-with-contentprovideroperation/ http://www.grokkingandroid.com/androids-contentprovideroperation-withbackreference-explained/ What are the semantics of withValueBackReference?

But notice, using ContentProviderOperation is much slower than bulkInsert() if you are inserting many rows at once, as it parses Uri (string comparisions) each time the operation is going to be performed. Doing this way you still have to expose Uri for inserting into child table.

If you decide to use applyBatch(), overwrite it in your provider so it performs all operations in one transaction, so you retain consistency in data and speed up database operations:

@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {
    final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    db.beginTransaction();
    try {
        ContentProviderResult[] results = super.applyBatch(operations);
        db.setTransactionSuccessful();
        return results;
    } finally {
        db.endTransaction();
    }
}
Community
  • 1
  • 1
biegleux
  • 13,179
  • 11
  • 45
  • 52
  • so the only way of not exposing child tables is using `applyBatch` ? – Gal Ben-Haim Jul 15 '12 at 14:05
  • You still need to define uri for inserting into child table, that's the way ContentProvider is acessed (you can throw IllegalArgumentException for query() operation on child table if you want to disallow querying child tables), acces to content providers is based on Uris and providers are primarily used for one table, they have limitations when it comes to relationships between tables, but there is nothing wrong in exposing separate uri for inserting into child table, posted applyBatch() method helps you prepare your operations and execute them in one transaction – biegleux Jul 15 '12 at 14:18
  • it's a trade-off using content provider instead of own database adapter implementation – biegleux Jul 15 '12 at 14:21
  • is it a waste of time using a `ContentProvider` if I'm not going to expose my data to other applications? I'm caching a complex data structure from a webservice and doing complex queries to view and filter data. would I be better using a custom database adapter or maybe a `ContentProvider` with some sort of helper class to handle the inner tables ? in the end, I want to abstract database business logic from my Activities – Gal Ben-Haim Jul 15 '12 at 14:31
  • 2
    You can find here at SO many answers about pros and cons using CP vs db adapter. There are situations where it is good to stick with CP, e.g. CursorLoader works only with CP, CP makes accessing db from a service and an activity very easy as it handles thread-safety for you. On the other hand CP adds another layer in you app which consumes more resources, also complex queries may be problematic (only basic query, insert, update, delete methods, input data as ContentValue only). – biegleux Jul 15 '12 at 14:58
  • I would suggest you taking a look at ContactsProvider source in android as it also uses more tables, maybe that helps you decide. – biegleux Jul 15 '12 at 14:58
  • I'd like to put my opinion on this. I to had a similar problem trying to fix deprecated code. What I was told is to use ContentProvider. But now that I have way to much code to turn back, I'm sticking to it. But my advice is to only use ContentProvider for things that its necessary for... like sharing data to other apps. Other than that just use SQLiteDatabaseHelper. ContentProvider was only an advantage when it came to querying tables. But other than that it was exactly the same as if I would have used the Database directly with async or just creating a seperate thread. – Andy Jul 15 '12 at 19:18
1

You are free to insert to multiply tables as long as the values needed are provided.

For example:

ContentValues v = new ContentValues();
v.put("title","post1");
v.put("tag","tag1");
getProvider().insert(POST_URI,v);

In the implementation of insert, you could check if fields (tag) belongs to other table exists.If it does , it means that you should do extra works - insert tag first if it does not exist, set up correct association between the tag and the post just inserted.

You can check the source code of android contacts for the reference.

UPDATE:

To insert multiply tags, one hack-y way is to insert a comma separated string. THis is not elegant but it works.

pierrotlefou
  • 39,805
  • 37
  • 135
  • 175
  • a post has many tags so I need to pass an array or a collection of tags. I didn't see that its possible with ContentValues. – Gal Ben-Haim Jul 15 '12 at 13:48
  • @Gal: To insert multiply tags, one hack-y way is to insert a comma separated string. THis is not elegant but it works. – pierrotlefou Jul 17 '12 at 01:15
1

Just to get this right: You want to have one URI and insert a post and all its tags with one insert call to the ContentProvider? Correct?

The problem is, that you need to have all values in the ContentValues object. There is a reason for normalization in database. Nevertheless it might be doable. For tags this should be easy. Just use one String for all tags. For example "android, ios, bada, wp7" and parse this string in your insert method.

You could also use a naming plus integer convention. And as long as there is a tag1, tag2,... tagX you would read these values from within your ContentProvider's insert method.

Neither is elegant, but would work.

In this case bulkInsert or applyBatch have no place in your code. They come only into play, if you want to use multiple calls to your ContentProvider at once and within one transaction.

But I think the better solution would indeed be to actually use multiple operations as described by biegleux.

Wolfram Rittmeyer
  • 2,402
  • 1
  • 18
  • 21
0

Since you are going to be inserting into multiple tables the normal SQLiteDatabase.insert helper functions will not work. But this is completely doable in a performant and nice way.

You need to look at this from the endpoint of the user who is going to be inserting into you ContentProvider, even if it is only yourself. So first define the names or keys for all of you fields. Since you won't be using SQLiteDatabase.insert you don't actually need to name them the same as the database fields. None of the names should be duplicate. If for example you have fields in two different tables overlap perhaps tag in TableA and in TableB you could define the name for those field as TableA.tag and TableB.tag. Or use semantic naming for more descriptive names that don't collide.

Next you need to create your insert queries using SQLiteStatement per this answer. Make sure the names you use in createInsert are the same ones that the callers of the ContentProvider use as keys in the ContentValues.

Community
  • 1
  • 1
Jade
  • 3,156
  • 1
  • 34
  • 44