Android: дизайн SQLite one-to-many

У кого-нибудь есть хорошая рекомендация о том, как реализовать отображение "один ко многим" для SQLite с помощью ContentProvider? Если вы посмотрите Uri ContentProvider#insert(Uri, ContentValues), вы увидите, что у него есть параметр ContentValues, содержащий данные для вставки. Проблема в том, что в своей текущей реализации ContentValues не поддерживает метод put(String, Object), и класс является окончательным, поэтому я не могу его расширять. Почему это проблема? Вот мой дизайн:

У меня есть 2 таблицы, которые связаны друг с другом. Чтобы представить их в коде, у меня есть 2 объекта модели. Первая представляет основную запись и имеет поле, которое является списком экземпляров 2-го объекта. Теперь у меня есть вспомогательный метод в объекте модели # 1, который возвращает ContentValues, сгенерированный с текущего объекта. Тривиально заполнять примитивные поля перегруженными методами ContentValues#put, но мне не повезло в списке. Так что, так как моя вторая строка таблицы - это всего лишь одно значение String, я генерирую строку с разделителями-запятыми, которая затем я переписываю в String [] внутри ContentProvider#insert. Это кажется yucky, поэтому, может быть, кто-то может намекнуть, как это можно сделать чище.

Вот какой код. Сначала из класса модели:

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 */}

и здесь уменьшена версия метода ContentProvider#insert

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

Я думаю, что вы смотрите на неправильный конец отношений "один ко многим".

Посмотрите, например, на поставщика контента ContactsContract. У контактов может быть много адресов электронной почты, много телефонных номеров и т.д. То, как это делается, - это делать вставки/обновления/удаления со стороны "много". Чтобы добавить новый номер телефона, вы вставляете новый номер телефона, указав идентификатор контакта, для которого указан номер телефона.

Вы бы сделали то же самое, если бы у вас была простая база данных SQLite без поставщика контента. Соотношения "один ко многим" в реляционных базах данных достигаются с помощью вставок/обновлений/удалений в таблице для "многих" сторон, каждый из которых имеет внешний ключ обратно на "одну" сторону.

Теперь, с точки зрения ОО, это не идеально. Вы можете создавать объекты оболочки ORM (думаю, Hibernate), которые позволяют вам манипулировать коллекцией детей со стороны "одного". Затем достаточно интеллектуальный класс коллекции может развернуться и синхронизировать таблицу "many", чтобы соответствовать. Однако они не обязательно тривиальны для правильной реализации.

Ответ 2

Вы можете использовать ContentProviderOperations для этого.

В основном это массовые операции с возможностью обратной ссылки на идентификаторы, сгенерированные для родительских строк.

Как ContentProviderOperations может использоваться для дизайна "один ко многим", очень хорошо объясняется в этом ответе: Какова семантика withValueBackReference?

Ответ 3

Итак, я собираюсь ответить на свой вопрос. Я был на правильном пути с двумя таблицами и двумя объектами модели. То, что не хватало и что меня смутило, заключалось в том, что я хотел напрямую вставлять сложные данные через ContentProvider#insert за один вызов. Это не верно. ContentProvider должен создавать и поддерживать эти две таблицы, но решение о том, какая таблица использовать, должна определяться параметром Uri ContentProvider#insert. Очень удобно использовать ContentResolver и добавлять в объект модели такие методы, как "addFoo". Такой метод будет принимать параметр ContentResolver, а в конце - последовательность вставки сложной записи:

  • Вставьте родительскую запись через ContentProvider#insert и получите идентификатор записи
  • Каждому ребенку предоставляется родительский идентификатор (ключ foregn) и используйте ContentProvider#insert с разными Uri для вставки дочерних записей

Итак, остается только вопрос о том, как конвертировать вышеуказанный код в транзакцию?