Praktik terbaik untuk mengekspos beberapa tabel menggunakan penyedia konten di Android

90

Saya sedang membangun aplikasi tempat saya memiliki meja untuk acara dan meja untuk berbagai tempat. Saya ingin dapat memberikan akses aplikasi lain ke data ini. Saya punya beberapa pertanyaan terkait praktik terbaik untuk jenis masalah ini.

  1. Bagaimana saya harus menyusun kelas database? Saat ini saya memiliki kelas untuk EventsDbAdapter dan VenuesDbAdapter, yang menyediakan logika untuk membuat kueri setiap tabel, sambil memiliki DbManager terpisah (extends SQLiteOpenHelper) untuk mengelola versi database, membuat / mengupgrade database, memberikan akses ke database (getWriteable / ReadeableDatabase). Apakah ini solusi yang disarankan, atau apakah saya lebih baik mengkonsolidasikan semuanya ke satu kelas (mis. DbManager) atau memisahkan semuanya dan membiarkan setiap Adaptor memperluas SQLiteOpenHelper?

  2. Bagaimana cara mendesain penyedia konten untuk beberapa tabel? Memperluas pertanyaan sebelumnya, haruskah saya menggunakan satu Penyedia Konten untuk seluruh aplikasi, atau haruskah saya membuat penyedia terpisah untuk Acara dan Tempat?

Sebagian besar contoh yang saya temukan hanya berhubungan dengan aplikasi tabel tunggal, jadi saya akan menghargai petunjuk apa pun di sini.

Gunnar Lium
sumber

Jawaban:

114

Mungkin agak terlambat untuk Anda, tetapi orang lain mungkin menganggap ini berguna.

Pertama, Anda perlu membuat beberapa 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");

Kemudian Anda memperluas Pencocok URI Anda

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);      
}

Kemudian buat tabel Anda

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);";

Jangan lupa menambahkan yang kedua DATABASE_CREATEkeonCreate()

Anda akan menggunakan blok switch-case untuk menentukan tabel apa yang digunakan. Ini adalah kode sisipan saya

@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;                
}

Anda akan perlu untuk membagi up delete, update, getType, dll Dimanapun panggilan penyedia Anda untuk DATABASE_TABLE atau CONTENT_URI Anda akan menambahkan sebuah kasus dan memiliki DATABASE_TABLE1 atau CONTENT_URI1 dalam satu dan # 2 di depan dan seterusnya untuk sebanyak yang Anda inginkan.

Opy
sumber
1
Terima kasih atas jawaban Anda, ini cukup dekat dengan solusi yang akhirnya saya gunakan. Saya menemukan bahwa penyedia kompleks yang bekerja dengan beberapa tabel mendapatkan banyak pernyataan pengalih, yang tampaknya tidak terlalu elegan. Tapi saya mengerti itulah cara kebanyakan orang melakukannya.
Gunnar Lium
Apakah notifyChange benar-benar harus menggunakan _uri dan bukan uri asli?
rentang tanggal
18
Apakah ini standar yang diterima dengan Android? Ini bekerja, jelas, tetapi tampaknya sedikit "kikuk".
prolink007
Selalu bisa menggunakan pernyataan switch sebagai semacam router. Kemudian berikan metode terpisah untuk melayani setiap sumber daya. query, queryUsers, queryUser, queryGroups, queryGroup Ini adalah bagaimana built-in kontak penyedia melakukannya. com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex
2
Mengingat bahwa pertanyaan tersebut meminta rekomendasi untuk desain kelas database praktik terbaik, saya akan menambahkan bahwa tabel harus didefinisikan di kelas mereka sendiri, dengan anggota kelas negara bagian yang mengekspos atribut seperti nama tabel dan kolom.
MM.
10

Saya sarankan untuk memeriksa kode sumber untuk Android 2.x ContactProvider. (Yang dapat ditemukan secara online). Mereka menangani kueri tabel silang dengan menyediakan tampilan khusus yang kemudian Anda jalankan kueri di bagian belakang. Di front end, mereka dapat diakses oleh pemanggil melalui berbagai URI berbeda melalui satu penyedia konten. Anda mungkin juga ingin menyediakan satu atau dua kelas untuk menyimpan konstanta untuk nama bidang tabel dan string URI Anda. Kelas-kelas ini dapat disediakan baik sebagai API include atau sebagai drop in class, dan akan mempermudah penggunaan aplikasi yang memakannya.

Agak rumit, jadi Anda mungkin juga ingin memeriksa bagaimana kalender juga untuk mendapatkan gambaran tentang apa yang Anda lakukan dan tidak butuhkan.

Anda hanya memerlukan satu adaptor DB dan satu penyedia Materi per database (bukan per tabel) untuk melakukan sebagian besar pekerjaan, tetapi Anda dapat menggunakan beberapa adaptor / penyedia jika Anda benar-benar menginginkannya. Itu hanya membuat segalanya menjadi sedikit lebih rumit.

Charles B
sumber
5
com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex
@Marloke Terima kasih. Ok, saya mengerti bahwa bahkan tim Android menggunakan switchsolusi, tetapi bagian ini Anda sebutkan: 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. Apakah Anda pikir Anda bisa menjelaskannya sedikit lebih detail?
eddy
7

Satu ContentProviderdapat melayani banyak tabel, tetapi mereka harus agak terkait. Ini akan membuat perbedaan jika Anda ingin menyinkronkan penyedia Anda. Jika Anda ingin sinkronisasi terpisah, katakanlah Kontak, Email, atau Kalender, Anda akan memerlukan penyedia yang berbeda untuk masing-masing, meskipun mereka berada di database yang sama atau disinkronkan dengan layanan yang sama, karena Adaptor Sinkronisasi terikat langsung ke penyedia tertentu.

Sejauh yang saya tahu, Anda hanya dapat menggunakan satu SQLiteOpenHelper per database, karena ia menyimpan informasi meta dalam tabel di dalam database. Jadi, jika Anda ContentProvidersmengakses database yang sama, Anda harus membagikan Helper entah bagaimana caranya.

Timo Ohr
sumber
7

Catatan: Ini adalah klarifikasi / modifikasi atas jawaban yang diberikan oleh Opy.

Pendekatan ini membagi masing-masing insert, delete, update, dan getTypemetode dengan laporan beralih untuk menangani setiap tabel pribadi Anda. Anda akan menggunakan CASE untuk mengidentifikasi setiap tabel (atau uri) yang akan direferensikan. Setiap CASE kemudian dipetakan ke salah satu tabel atau URI Anda. Misalnya, TABLE1 atau URI1 dipilih dalam KASUS # 1, dll untuk semua tabel aplikasi Anda mempekerjakan.

Berikut contoh pendekatannya. Ini untuk metode penyisipan. Ini diterapkan sedikit berbeda dari Opy tetapi melakukan fungsi yang sama. Anda dapat memilih gaya yang Anda sukai. Saya juga ingin memastikan sisipan mengembalikan nilai bahkan jika penyisipan tabel gagal. Dalam hal ini mengembalikan a -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]
PeteH
sumber
3

Saya menemukan demo dan penjelasan terbaik untuk ContentProvider dan menurut saya itu telah mengikuti Standar Android.

Kelas Kontrak

 /**
   * 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";

dan Kelas Dalam:

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

Sekarang membuat Database menggunakan 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 + "));"
        );
    }
}

Penyedia konten:

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;
    }
}

Saya harap ini akan membantu Anda.

Demo di GitHub: https://github.com/androidessence/MovieDatabase

Artikel Lengkap: https://guides.codepath.com/android/creating-content-providers

Referensi:

Catatan: Saya menyalin kode hanya karena jika tautan demo atau artikel dapat dihapus di masa mendatang.

Pratik Butani
sumber