Android'de içerik sağlayıcıları kullanarak birden çok tabloyu göstermeye yönelik en iyi uygulamalar


90

Etkinlikler için bir masam ve mekanlar için bir masam olan bir uygulama oluşturuyorum. Diğer uygulamalara bu verilere erişim izni verebilmek istiyorum. Bu tür problemler için en iyi uygulamalarla ilgili birkaç sorum var.

  1. Veritabanı sınıflarını nasıl yapılandırmalıyım? Şu anda EventsDbAdapter ve VenuesDbAdapter için her tabloyu sorgulamak için mantık sağlayan, veritabanı sürümlerini yönetmek, veritabanları oluşturmak / yükseltmek ve veritabanına erişim sağlamak için ayrı bir DbManager (SQLiteOpenHelper'ı genişletir) bulunduran sınıflara sahibim (getWriteable / ReadeableDatabase). Bu önerilen çözüm mü yoksa her şeyi tek bir sınıfa (yani DbManager) konsolide etmek veya her şeyi ayırmak ve her Adaptörün SQLiteOpenHelper'ı genişletmesine izin vermek daha mı iyi?

  2. Birden çok tablo için içerik sağlayıcıları nasıl tasarlamalıyım? Önceki soruyu genişleterek, tüm uygulama için bir İçerik Sağlayıcı mı kullanmalıyım yoksa Etkinlikler ve Mekanlar için ayrı sağlayıcılar mı oluşturmalıyım?

Bulduğum çoğu örnek yalnızca tek tablalı uygulamalarla ilgileniyor, bu yüzden buradaki herhangi bir öneriyi takdir ediyorum.

Yanıtlar:


114

Muhtemelen sizin için biraz geç, ancak diğerleri bunu faydalı bulabilir.

Öncelikle birden fazla CONTENT_URI oluşturmanız gerekir

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

Ardından URI Eşleştiricinizi genişletirsiniz

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

Ardından tablolarınızı oluşturun

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

İkinci eklemeyi unutmayın DATABASE_CREATEiçinonCreate()

Hangi tablonun kullanıldığını belirlemek için bir anahtar durum bloğu kullanacaksınız. Bu benim ekleme kodum

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

En fazla bölmek gerekir delete, update, getTypevb birçok olarak ve böylece bir sonraki içinde 2. bir olgu eklemek ve var DATABASE_TABLE1 veya CONTENT_URI1 birinde olacak ve DATABASE_TABLE veya CONTENT_URI için sağlayıcı çağrıları istediğiniz gibi her yerde.


1
Cevabınız için teşekkürler, bu, kullandığım çözüme oldukça yakındı. Birkaç tabloyla çalışan karmaşık sağlayıcıların pek çok anahtar ifadesi aldığını görüyorum ki bu o kadar da zarif görünmüyor. Ama anlıyorum ki çoğu insan bunu yapıyor.
Gunnar Lium

NotifyChange'in gerçekten orijinal uri'yi değil _uri'yi kullanması mı gerekiyor?
yayılma

18
Bu, Android'de kabul edilen standart mı? Açıkçası işe yarıyor, ancak biraz "hantal" görünüyor.
prolink007

Switch komutlarını her zaman bir çeşit yönlendirici olarak kullanabilir. Ardından, her bir kaynağa hizmet vermek için ayrı yöntemler sağlayın. query, queryUsers, queryUser, queryGroups, queryGroup Bu, yerleşik kişiler sağlayıcı da bu yöntemi kullanıyor. com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex

2
Sorunun en iyi uygulama veritabanı sınıfı tasarımı için öneri talep ettiği göz önüne alındığında, tabloların kendi sınıflarında tanımlanması gerektiğini ve durum sınıfı üyelerinin tablo ve sütun adı gibi öznitelikleri açığa çıkarması gerektiğini ekleyeceğim.
MM.

10

Android 2.x ContactProvider için kaynak kodunu kontrol etmenizi öneririm. (Hangi çevrimiçi bulunabilir). Daha sonra arka uçta sorgu çalıştırdığınız özel görünümler sağlayarak çapraz tablo sorgularını ele alırlar. Ön uçta, tek bir içerik sağlayıcı aracılığıyla çeşitli farklı URI'ler aracılığıyla arayan tarafından erişilebilirler. Muhtemelen tablo alan adlarınız ve URI dizeleriniz için sabitleri tutmak için bir veya iki sınıf da sağlamak isteyeceksiniz. Bu sınıflar, API dahil olarak veya sınıfta bırakılarak sağlanabilir ve tüketen uygulamanın kullanımını çok daha kolay hale getirecektir.

Biraz karmaşık, bu yüzden ne yaptığınız ve neye ihtiyacınız olmadığına dair bir fikir edinmek için takvimin nasıl olduğuna da göz atmak isteyebilirsiniz.

İşin çoğunu yapmak için yalnızca tek bir DB bağdaştırıcısına ve veritabanı başına tek bir İçerik sağlayıcısına (tablo başına değil) ihtiyacınız vardır, ancak gerçekten isterseniz birden çok bağdaştırıcı / sağlayıcı kullanabilirsiniz. Sadece işleri biraz daha karmaşık hale getiriyor.


5
com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex

@Marloke Teşekkürler. Tamam, ben bile Android ekibi anlamak kullanmak switchçözüm, ancak bu kısım Bahsettiğiniz: 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. Bunu biraz daha detaylı açıklayabilir misin?
eddy

7

Bir ContentProvider birden fazla tablo hizmet edebilir, ancak bunlar biraz ilgili olmalıdır. Sağlayıcılarınızı senkronize etmeyi düşünüyorsanız bir fark yaratacaktır. Diyelim ki Kişiler, Posta veya Takvim için ayrı eşitleme istiyorsanız, aynı veritabanında bulunsalar veya aynı hizmetle eşitlenmiş olsalar bile her biri için farklı sağlayıcılara ihtiyacınız olacaktır, çünkü Eşitleme Adaptörleri doğrudan belirli bir sağlayıcı.

Anlayabildiğim kadarıyla, meta bilgilerini veritabanındaki bir tabloda sakladığı için veritabanı başına yalnızca tek bir SQLiteOpenHelper kullanabilirsiniz. Dolayısıyla ContentProviders, aynı veritabanına erişiminiz varsa , Yardımcısı bir şekilde paylaşmanız gerekir.


7

Not: Bu, Opy tarafından sağlanan cevaba yapılan bir açıklama / değişikliktir.

Bu yaklaşım, her subdivides insert, delete, updateve getTypebireysel Tablolarının her işlemek amacıyla anahtar ifadelere yöntemleri. Başvurulacak her tabloyu (veya uri'yi) tanımlamak için bir CASE kullanacaksınız. Her bir CASE daha sonra tablolarınızdan veya URI'lerinizden biriyle eşleşir. Örneğin, TABLO1 veya URI1 uygulama istihdam tüm tablolar için vb HALİNDE 1. seçilir.

İşte yaklaşımın bir örneği. Bu, ekleme yöntemi içindir. Opy's'den biraz farklı uygulanır ancak aynı işlevi yerine getirir. Tercih ettiğiniz stili seçebilirsiniz. Ayrıca, tablo ekleme başarısız olsa bile insertin bir değer döndürdüğünden emin olmak istedim. Bu durumda bir -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]

3

ContentProvider için en iyi demoyu ve açıklamayı buldum ve Android Standartlarını izlediğini düşünüyorum.

Sözleşme Sınıfları

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

ve İç Sınıflar:

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

Şimdi SQLiteOpenHelper kullanarak Veritabanı oluşturma :

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

İçerik sağlayıcı:

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

Umarım size yardımcı olur.

GitHub Demosu : https://github.com/androidessence/MovieDatabase

Tam Makale: https://guides.codepath.com/android/creating-content-providers

Referanslar:

Not: Kodu, demo veya makale bağlantısı ileride kaldırılabileceği için kopyaladım.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.