views:

1094

answers:

2

Anyone has good advise on how to implement one-to-many mapping for SQLite using ContentProvider? If you look at Uri ContentProvider#insert(Uri, ContentValues) you can see that it has ContentValues param that contains data to insert. The problem is that in its current implementation ContentValues does not support put(String, Object) method and class is final so I cannot extend it. Why it is a problem? Here comes my design:

I have 2 tables which are in one-to-many relationship. To represent these in code I have 2 model objects. 1st represents the main record and has a field that is a list of 2nd object instances. Now I have a helper method in the model object #1 which returns ContentValues generated off the current object. It's trivial to populate a primitive fields with ContentValues#put overloaded methods but I'm out of luck for the list. So currently since my 2nd table row is just a single String value I generate a comma delimited String which then I reparse to String[] inside ContentProvider#insert. That feels yucky, so maybe someone can hint how it can be done in cleaner fashion.

Here's some code. First from the model class:

public ContentValues toContentValues() {
    ContentValues values = new ContentValues();
    values.put(ITEM_ID, itemId);
    values.put(NAME, name);
    values.put(TYPES, concat(types));
    return values;
}

private String concat(String[] values) { /* trivial */}

and here's slimmed down version of ContentProvider#insert method

public Uri insert(Uri uri, ContentValues values) {
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    db.beginTransaction();
    try {
        // populate types
        String[] types = ((String)values.get(Offer.TYPES)).split("|");
        // we no longer need it
        values.remove(Offer.TYPES);
        // first insert row into OFFERS
        final long rowId = db.insert("offers", Offer.NAME, values);
        if (rowId > 0 && types != null) {
            // now insert all types for the row
            for (String t : types) {
                ContentValues type = new ContentValues(8);
                type.put(Offer.OFFER_ID, rowId);
                type.put(Offer.TYPE, t);
                // insert values into second table
                db.insert("types", Offer.TYPE, type);
            }
        }
        db.setTransactionSuccessful();
        return ContentUris.withAppendedId(Offer.CONTENT_URI, rowId);
    } catch (Exception e) {
        Log.e(TAG, "Failed to insert record", e);
    } finally {
        db.endTransaction();
    }

}
+1  A: 

I think you're looking at the wrong end of the one-to-many relationship.

Take a look at the ContactsContract content provider, for example. Contacts can have many email addresses, many phone numbers, etc. The way that is accomplished is by doing inserts/updates/deletes on the "many" side. To add a new phone number, you insert a new phone number, providing an ID of the contact for whom the phone number pertains.

You would do the same if you had a plain SQLite database with no content provider. One-to-many relationships in relational databases are achieved via inserts/updates/deletes on a table for the "many" side, each having a foreign key back to the "one" side.

Now, from an OO standpoint, this isn't ideal. You are welcome to create ORM-style wrapper objects (think Hibernate) that allow you to manipulate a collection of children from the "one" side. A sufficiently-intelligent collection class can then turn around and synchronize the "many" table to match. However, these aren't necessarily trivial to implement properly.

CommonsWare
Well I have ONE main record, say Person which references MANY phone munbers. So I insert Person record and get its key then I insert bnuch of record into the Phone table supplying each one with Person foreign key. Pretty standard stuff. Then I have no problem representing this relation on Java side with two of my model objects and collection of Phones in the Person object. Where I do have hard time is to plug these into Provider API. I'm going to study ContactsContract, thank you for the hint and report back
DroidIn.net
Actually my inserts are not dynamic, basically I parse XML and insert records. Once there I will only read these without any further manipulation. I suppose I can use one ContentResolver per table but I also want to expose this functionality as ContentProvider and that's where again I'm having hard time
DroidIn.net
Actually I'm going through Contacts class in 1.5 - thanks again for the tip I think that is exactly what I need
DroidIn.net
Going to accept this since it's the only answer and I got inspired :)
DroidIn.net
+1  A: 

So I'm going to answer my own question. I was on the right track with having two tables and two model objects. What was missing and what confused me was that I wanted directly insert complex data through ContentProvider#insert in a single call. This is wrong. ContentProvider should create and maintain these two tables but decision on which table to use should be dictated by Uri parameter of ContentProvider#insert. It is very convenient to use ContentResolver and add methods such as "addFoo" to the model object. Such method would take ContentResolver parameter and at the end here are the sequence to insert a complex record:

  1. Insert parent record through ContentProvider#insert and obtain record id
  2. Per each child provide parent ID (foregn key) and use ContentProvider#insert with different Uri to insert child records

So the only remaining question is how to envelope the above code in transaction?

DroidIn.net