Лучшие практики для демонстрации нескольких таблиц с помощью контент-провайдеров в Android

Я создаю приложение, в котором у меня есть стол для событий и стол для мероприятий. Я хочу иметь возможность предоставить другим приложениям доступ к этим данным. У меня есть несколько вопросов, связанных с лучшими практиками для такого рода проблем.

Как я должен структурировать классы базы данных? В настоящее время у меня есть классы для EventsDbAdapter и VenuesDbAdapter, которые предоставляют логику для запроса каждой таблицы, в то время как у меня есть отдельный DbManager (расширяет SQLiteOpenHelper) для управления версиями базы данных, создания / обновления баз данных, предоставления доступа к базе данных (getWriteable / ReadeableDatabase). Является ли это рекомендуемым решением, или мне было бы лучше либо объединить все в один класс (т.е. DbManager), либо разделить все и позволить каждому адаптеру расширять SQLiteOpenHelper?

Как мне спроектировать контент-провайдеров для нескольких таблиц? Продолжая предыдущий вопрос, должен ли я использовать один контент-провайдер для всего приложения или мне нужно создать отдельных провайдеров для событий и мест проведения?

Большинство примеров, которые я нахожу, касаются только приложений с одним столом, поэтому я был бы признателен за любые указатели здесь.

Ответы на вопрос(5)

Который можно найти в Интернете). Они обрабатывают запросы между таблицами, предоставляя специализированные представления, с которыми вы затем выполняете запросы на серверной части. На внешнем интерфейсе они доступны для вызывающего абонента через различные URI через одного поставщика контента. Возможно, вы также захотите предоставить один или два класса для хранения констант для имен полей таблицы и строк URI. Эти классы могут быть предоставлены как в виде включения API, так и в виде класса исключения, что значительно облегчит использование приложения-потребителя.

Это немного сложно, поэтому вы также можете проверить, как работает календарь, чтобы получить представление о том, что вы делаете, а что нет.

Для выполнения большей части работы вам потребуется только один адаптер БД и один поставщик контента на базу данных (не на таблицу), но вы можете использовать несколько адаптеров / поставщиков, если вы действительно этого хотите. Это только усложняет ситуацию.

 Alex27 февр. 2013 г., 20:29
com.android.providers.contacts.ContactsProvider2.javagithub.com/android/platform_packages_providers_contactsprovider/...
 eddy01 дек. 2014 г., 13:50
@ Марлок Спасибо. Хорошо, я понимаю, что даже команда Android используетswitch решение, но эту часть вы упомянули:They handle cross table queries by providing specialized views that you then run queries against on the back end. On the front end they are accessible to the caller via various different URIs through a single content provider , Как вы думаете, вы могли бы объяснить это немного подробнее?

ContentProvider может обслуживать несколько таблиц, но они должны быть несколько связаны. Это будет иметь значение, если вы собираетесь синхронизировать своих провайдеров. Если вам нужна отдельная синхронизация для, скажем, контактов, почты или календаря, вам понадобятся разные провайдеры для каждого из них, даже если они в конечном итоге находятся в одной базе данных или синхронизируются с одной и той же службой, поскольку адаптеры синхронизации напрямую связаны с конкретный поставщик.

Насколько я могу судить, вы можете использовать только один SQLiteOpenHelper для каждой базы данных, поскольку он хранит свою метаинформацию в таблице в базе данных. Так что если вашContentProviders доступ к той же базе данных, вам придется каким-то образом поделиться Помощник.

Этот подход подразделяет каждый изinsert, delete, update, а такжеgetType методы с инструкциями switch для обработки каждой из ваших отдельных таблиц. Вы будете использовать CASE для идентификации каждой таблицы (или URI), на которую будут ссылаться. Каждый CASE затем отображается на одну из ваших таблиц или URI. Например.,ТАБЛИЦА 1 или жеURI1 выбран в CASE # 1 и т. д. для всех таблиц, которые использует ваше приложение.

Вот пример подхода. Это для метода вставки. Он реализован немного иначе, чем Opy, но выполняет ту же функцию. Вы можете выбрать стиль, который вы предпочитаете. Я также хотел убедиться, что вставка возвращает значение, даже если вставка таблицы не удалась. В этом случае он возвращает-1.

  @Override
  public Uri insert(Uri uri, ContentValues values) {
    int uriType = sURIMatcher.match(uri);
    SQLiteDatabase sqlDB; 

    long id = 0;
    switch (uriType){ 
        case TABLE1: 
            sqlDB = Table1Database.getWritableDatabase();
            id = sqlDB.insert(Table1.TABLE_NAME, null, values); 
            getContext().getContentResolver().notifyChange(uri, null);
            return Uri.parse(BASE_PATH1 + "/" + id);
        case TABLE2: 
            sqlDB = Table2Database.getWritableDatabase();
            id = sqlDB.insert(Table2.TABLE_NAME, null, values); 
            getContext().getContentResolver().notifyChange(uri, null);
            return Uri.parse(BASE_PATH2 + "/" + id);
        default: 
            throw new SQLException("Failed to insert row into " + uri); 
            return -1;
    }       
  }  // [END insert]

Сначала вам нужно создать несколько CONTENT_URI

public static final Uri CONTENT_URI1 = 
    Uri.parse("content://"+ PROVIDER_NAME + "/sampleuri1");
public static final Uri CONTENT_URI2 = 
    Uri.parse("content://"+ PROVIDER_NAME + "/sampleuri2");

Затем вы расширяете свой URI Matcher

private static final UriMatcher uriMatcher;
static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI(PROVIDER_NAME, "sampleuri1", SAMPLE1);
    uriMatcher.addURI(PROVIDER_NAME, "sampleuri1/#", SAMPLE1_ID);      
    uriMatcher.addURI(PROVIDER_NAME, "sampleuri2", SAMPLE2);
    uriMatcher.addURI(PROVIDER_NAME, "sampleuri2/#", SAMPLE2_ID);      
}

Затем создайте свои таблицы

private static final String DATABASE_NAME = "sample.db";
private static final String DATABASE_TABLE1 = "sample1";
private static final String DATABASE_TABLE2 = "sample2";
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_CREATE1 =
    "CREATE TABLE IF NOT EXISTS " + DATABASE_TABLE1 + 
    " (" + _ID1 + " INTEGER PRIMARY KEY AUTOINCREMENT," + 
    "data text, stuff text);";
private static final String DATABASE_CREATE2 =
    "CREATE TABLE IF NOT EXISTS " + DATABASE_TABLE2 + 
    " (" + _ID2 + " INTEGER PRIMARY KEY AUTOINCREMENT," + 
    "data text, stuff text);";

Не забудьте добавить второйDATABASE_CREATE вonCreate()

Вы собираетесь использоватьПереключатель регистра блок, чтобы определить, какая таблица используется. Это мой код вставки

@Override
public Uri insert(Uri uri, ContentValues values) {
    Uri _uri = null;
    switch (uriMatcher.match(uri)){
    case SAMPLE1:
        long _ID1 = db.insert(DATABASE_TABLE1, "", values);
        //---if added successfully---
        if (_ID1 > 0) {
            _uri = ContentUris.withAppendedId(CONTENT_URI1, _ID1);
            getContext().getContentResolver().notifyChange(_uri, null);    
        }
        break;
    case SAMPLE2:
        long _ID2 = db.insert(DATABASE_TABLE2, "", values);
        //---if added successfully---
        if (_ID2 > 0) {
            _uri = ContentUris.withAppendedId(CONTENT_URI2, _ID2);
            getContext().getContentResolver().notifyChange(_uri, null);    
        }
        break;
    default: throw new SQLException("Failed to insert row into " + uri);
    }
    return _uri;                
}

Вам нужно будет разделитьdelete, update, getTypeи т. д. Везде, где ваш провайдер вызывает DATABASE_TABLE или CONTENT_URI, вы добавляете дело, и у вас есть DATABASE_TABLE1 или CONTENT_URI1 в одном и № 2 в следующем и так далее столько, сколько вы хотите.

 Luis Pena27 июл. 2014 г., 18:33
@MM. Не могли бы вы привести пример, используя эту технику?
 span29 авг. 2012 г., 10:40
Действительно ли notifyChange должен использовать _uri, а не оригинальный uri?
 prolink00703 сент. 2012 г., 17:28
Это принятый стандарт с Android? Это работает, очевидно, но кажется немного "неуклюжим".
 Gunnar Lium09 дек. 2010 г., 11:28
Спасибо за ваш ответ, это было довольно близко к решению, которое я в конечном итоге использовал. Я нахожу, что сложные провайдеры, работающие с несколькими таблицами, получают много операторов switch, что не выглядит слишком элегантно. Но я понимаю, что так делают большинство людей.
 Raimi bin Karim10 окт. 2017 г., 08:01
@ prolink007 Да, есть онлайн-курс по хранению данных Android в Udacity (который преподается самим Google), который занимается этим.udacity.com/course/android-basics-data-storage--ud845
 MM.02 мар. 2014 г., 19:33
Учитывая, что вопрос требует рекомендации для разработки класса базы данных наилучшей практики, я бы добавил, что таблицы должны быть определены в своем собственном классе, а члены класса состояний предоставляют такие атрибуты, как имя таблицы и столбца.
 Alex27 февр. 2013 г., 20:32
Всегда можно просто использовать операторы switch в качестве своего рода маршрутизатора. Затем предоставьте отдельные методы для обслуживания каждого ресурса.query, queryUsers, queryUser, queryGroups, queryGroup  Вот как это делает встроенный поставщик контактов. com.android.providers.contacts.ContactsProvider2.javagithub.com/android/platform_packages_providers_contactsprovider/...

Поставщик услуг и я думаю, что это следовало стандартам Android.

Контрактные классы

 /**
   * The Content Authority is a name for the entire content provider, similar to the relationship
   * between a domain name and its website. A convenient string to use for content authority is
   * the package name for the app, since it is guaranteed to be unique on the device.
   */
  public static final String CONTENT_AUTHORITY = "com.androidessence.moviedatabase";

  /**
   * The content authority is used to create the base of all URIs which apps will use to
   * contact this content provider.
   */
  private static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);

  /**
   * A list of possible paths that will be appended to the base URI for each of the different
   * tables.
   */
  public static final String PATH_MOVIE = "movie";
  public static final String PATH_GENRE = "genre";

а такжеВнутренние классы:

 /**
   * Create one class for each table that handles all information regarding the table schema and
   * the URIs related to it.
   */
  public static final class MovieEntry implements BaseColumns {
      // Content URI represents the base location for the table
      public static final Uri CONTENT_URI =
              BASE_CONTENT_URI.buildUpon().appendPath(PATH_MOVIE).build();

      // These are special type prefixes that specify if a URI returns a list or a specific item
      public static final String CONTENT_TYPE =
              "vnd.android.cursor.dir/" + CONTENT_URI  + "/" + PATH_MOVIE;
      public static final String CONTENT_ITEM_TYPE =
              "vnd.android.cursor.item/" + CONTENT_URI + "/" + PATH_MOVIE;

      // Define the table schema
      public static final String TABLE_NAME = "movieTable";
      public static final String COLUMN_NAME = "movieName";
      public static final String COLUMN_RELEASE_DATE = "movieReleaseDate";
      public static final String COLUMN_GENRE = "movieGenre";

      // Define a function to build a URI to find a specific movie by it's identifier
      public static Uri buildMovieUri(long id){
   ,       return ContentUris.withAppendedId(CONTENT_URI, id);
      }
  }

  public static final class GenreEntry implements BaseColumns{
      public static final Uri CONTENT_URI =
              BASE_CONTENT_URI.buildUpon().appendPath(PATH_GENRE).build();

      public static final String CONTENT_TYPE =
              "vnd.android.cursor.dir/" + CONTENT_URI + "/" + PATH_GENRE;
      public static final String CONTENT_ITEM_TYPE =
              "vnd.android.cursor.item/" + CONTENT_URI + "/" + PATH_GENRE;

      public static final String TABLE_NAME = "genreTable";
      public static final String COLUMN_NAME = "genreName";

      public static Uri buildGenreUri(long id){
          return ContentUris.withAppendedId(CONTENT_URI, id);
      }
  }

Теперь создание базы данных с использованиемSQLiteOpenHelper:

public class MovieDBHelper extends SQLiteOpenHelper{
    /**
     * Defines the database version. This variable must be incremented in order for onUpdate to
     * be called when necessary.
     */
    private static final int DATABASE_VERSION = 1;
    /**
     * The name of the database on the device.
     */
    private static final String DATABASE_NAME = "movieList.db";

    /**
     * Default constructor.
     * @param context The application context using this database.
     */
    public MovieDBHelper(Context context){
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    /**
     * Called when the database is first created.
     * @param db The database being created, which all SQL statements will be executed on.
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        addGenreTable(db);
        addMovieTable(db);
    }

    /**
     * Called whenever DATABASE_VERSION is incremented. This is used whenever schema changes need
     * to be made or new tables are added.
     * @param db The database being updated.
     * @param oldVersion The previous version of the database. Used to determine whether or not
     *                   certain updates should be run.
     * @param newVersion The new version of the database.
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }

    /**
     * Inserts the genre table into the database.
     * @param db The SQLiteDatabase the table is being inserted into.
     */
    private void addGenreTable(SQLiteDatabase db){
        db.execSQL(
                "CREATE TABLE " + MovieContract.GenreEntry.TABLE_NAME + " (" +
                        MovieContract.GenreEntry._ID + " INTEGER PRIMARY KEY, " +
                        MovieContract.GenreEntry.COLUMN_NAME + " TEXT UNIQUE NOT NULL);"
        );
    }

    /**
     * Inserts the movie table into the database.
     * @param db The SQLiteDatabase the table is being inserted into.
     */
    private void addMovieTable(SQLiteDatabase db){
        db.execSQL(
                "CREATE TABLE " + MovieContract.MovieEntry.TABLE_NAME + " (" +
                        MovieContract.MovieEntry._ID + " INTEGER PRIMARY KEY, " +
                        MovieContract.MovieEntry.COLUMN_NAME + " TEXT NOT NULL, " +
                        MovieContract.MovieEntry.COLUMN_RELEASE_DATE + " TEXT NOT NULL, " +
                        MovieContract.MovieEntry.COLUMN_GENRE + " INTEGER NOT NULL, " +
                        "FOREIGN KEY (" + MovieContract.MovieEntry.COLUMN_GENRE + ") " +
                        "REFERENCES " + MovieContract.GenreEntry.TABLE_NAME + " (" + MovieContract.GenreEntry._ID + "));"
        );
    }
}

Поставщик услуг:

public class MovieProvider extends ContentProvider {
    // Use an int for each URI we will run, this represents the different queries
    private static final int GENRE = 100;
    private static final int GENRE_ID = 101;
    private static final int MOVIE = 200;
    private static final int MOVIE_ID = 201;

    private static final UriMatcher sUriMatcher = buildUriMatcher();
    private MovieDBHelper mOpenHelper;

    @Override
    public boolean onCreate() {
        mOpenHelper = new MovieDBHelper(getContext());
        return true;
    }

    /**
     * Builds a UriMatcher that is used to determine witch database request is being made.
     */
    public static UriMatcher buildUriMatcher(){
        String content = MovieContract.CONTENT_AUTHORITY;

        // All paths to the UriMatcher have a corresponding code to return
        // when a match is found (the ints above).
        UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
        matcher.addURI(content, MovieContract.PATH_GENRE, GENRE);
        matcher.addURI(content, MovieContract.PATH_GENRE + "/#", GENRE_ID);
        matcher.addURI(content, MovieContract.PATH_MOVIE, MOVIE);
        matcher.addURI(content, MovieContract.PATH_MOVIE + "/#", MOVIE_ID);

        return matcher;
    }

    @Override
    public String getType(Uri uri) {
        switch(sUriMatcher.match(uri)){
            case GENRE:
                return MovieContract.GenreEntry.CONTENT_TYPE;
            case GENRE_ID:
                return MovieContract.GenreEntry.CONTENT_ITEM_TYPE;
            case MOVIE:
                return MovieContract.MovieEntry.CONTENT_TYPE;
            case MOVIE_ID:
                return MovieContract.MovieEntry.CONTENT_ITEM_TYPE;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        Cursor retCursor;
        switch(sUriMatcher.match(uri)){
            case GENRE:
                retCursor = db.query(
                        MovieContract.GenreEntry.TABLE_NAME,
                        projection,
                        selection,
                        selectionArgs,
                        null,
                        null,
                        sortOrder
                );
                break;
            case GENRE_ID:
                long _id = ContentUris.parseId(uri);
                retCursor = db.query(
                        MovieContract.GenreEntry.TABLE_NAME,
                        projection,
                        MovieContract.GenreEntry._ID + " = ?",
                        new String[]{String.valueOf(_id)},
                        null,
                        null,
                        sortOrder
                );
                break;
            case MOVIE:
                retCursor = db.query(
                        MovieContract.MovieEntry.TABLE_NAME,
                        projection,
                        selection,
                        selectionArgs,
                        null,
                        null,
                        sortOrder
                );
                break;
            case MOVIE_ID:
                _id = ContentUris.parseId(uri);
                retCursor = db.query(
                        MovieContract.MovieEntry.TABLE_NAME,
                        projection,
                        MovieContract.MovieEntry._ID + " = ?",
                        new String[]{String.valueOf(_id)},
                        null,
                        null,
                        sortOrder
                );
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        // Set the notification URI for the cursor to the one passed into the function. This
        // causes the cursor to register a content observer to watch for changes that happen to
        // this URI and any of it's descendants. By descendants, we mean any URI that begins
        // with this path.
        retCursor.setNotificationUri(getContext().getContentResolver(), uri);
        return retCursor;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        long _id;
        Uri returnUri;

        switch(sUriMatcher.match(uri)){
            case GENRE:
                _id = db.insert(MovieContract.GenreEntry.TABLE_NAME, null, values);
                if(_id > 0){
                    returnUri =  MovieContract.GenreEntry.buildGenreUri(_id);
                } else{
                    throw new UnsupportedOperationException("Unable to insert rows into: " + uri);
                }
                break;
            case MOVIE:
                _id = db.insert(MovieContract.MovieEntry.TABLE_NAME, null, values);
                if(_id > 0){
                    returnUri = MovieContract.MovieEntry.buildMovieUri(_id);
                } else{
                    throw new UnsupportedOperationException("Unable to insert rows into: " + uri);
                }
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        // Use this on the URI passed into the function to notify any observers that the uri has
        // changed.
        getContext().getContentResolver().notifyChange(uri, null);
        return returnUri;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int rows; // Number of rows effected

        switch(sUriMatcher.match(uri)){
            case GENRE:
                rows = db.delete(MovieContract.GenreEntry.TABLE_NAME, selection, selectionArgs);
                break;
            case MOVIE:
                rows = db.delete(MovieContract.MovieEntry.TABLE_NAME, selection, selectionArgs);
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        // Because null could delete all rows:
        if(selection == null || rows != 0){
            getContext().getContentResolver().notifyChange(uri, null);
        }

        return rows;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int rows;

        switch(sUriMatcher.match(uri)){
            case GENRE:
                rows = db.update(MovieContract.GenreEntry.TABLE_NAME, values, selection, selectionArgs);
                break;
            case MOVIE:
                rows = db.update(MovieContract.MovieEntry.TABLE_NAME, values, selection, selectionArgs);
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        if(rows != 0){
            getContext().getContentResolver().notifyChange(uri, null);
        }

        return rows;
    }
}

Я надеюсь, что это поможет вам.

Демо наGitHub:https://github.com/androidessence/MovieDatabase

Полная статья:https://guides.codepath.com/android/creating-content-providers

Рекомендации:

http://code.tutsplus.com/tutorials/android-sdk_content-providers--mobile-5549

http://www.grokkingandroid.com/android-tutorial-writing-your-own-content-provider/

http://developer.android.com/guide/topics/providers/content-providers.html

https://thenewcircle.com/s/post/1375/android_content_provider_tutorial

http://www.grokkingandroid.com/android-tutorial-content-provider-basics/

http://androidessence.com/

Примечание: я скопировал код только потому, что в будущем возможно удаление ссылки из демонстрации или статьи.

Ваш ответ на вопрос