Najlepsze rozwiązania dotyczące ujawniania wielu tabel przy użyciu dostawców zawartości w systemie Android

90

Tworzę aplikację, w której mam stół na imprezy i stół na miejsca. Chcę mieć możliwość udzielania innym aplikacjom dostępu do tych danych. Mam kilka pytań dotyczących sprawdzonych metod rozwiązywania tego rodzaju problemów.

  1. Jak mam zorganizować klasy bazy danych? Obecnie mam klasy dla EventsDbAdapter i VewhereDbAdapter, które zapewniają logikę do wykonywania zapytań dla każdej tabeli, mając jednocześnie oddzielny DbManager (rozszerza SQLiteOpenHelper) do zarządzania wersjami bazy danych, tworzenia / aktualizowania baz danych, zapewniania dostępu do bazy danych (getWriteable / ReadeableDatabase). Czy jest to zalecane rozwiązanie, czy lepiej byłoby skonsolidować wszystko w jednej klasie (np. DbManager) lub oddzielić wszystko i pozwolić każdemu adapterowi rozszerzyć SQLiteOpenHelper?

  2. Jak zaprojektować dostawców zawartości dla wielu tabel? Rozszerzając poprzednie pytanie, czy powinienem używać jednego dostawcy treści dla całej aplikacji, czy też powinienem utworzyć oddzielnych dostawców dla wydarzeń i miejsc?

Większość przykładów, które znajduję, dotyczy tylko aplikacji z pojedynczym stołem, więc byłbym wdzięczny za wszelkie wskazówki.

Gunnar Lium
źródło

Odpowiedzi:

114

Prawdopodobnie jest trochę za późno, ale inni mogą uznać to za przydatne.

Najpierw musisz utworzyć wiele 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");

Następnie rozszerzaj swój 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);      
}

Następnie stwórz swoje stoły

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

Nie zapomnij dodać drugiego DATABASE_CREATEdoonCreate()

Zamierzasz użyć bloku przełączników, aby określić, która tabela jest używana. To jest mój kod wstawiania

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

Trzeba będzie podzielić w górę delete, update, getType, itd. Wszędzie tam, gdzie twoi dostawcy połączeń dla DATABASE_TABLE lub CONTENT_URI dodasz przypadek i mają DATABASE_TABLE1 lub CONTENT_URI1 w jednym i # 2 w następny i tak dalej, tak wiele jak chcesz.

Opy
źródło
1
Dziękuję za odpowiedź, to było bardzo zbliżone do rozwiązania, którego użyłem. Uważam, że złożeni dostawcy pracujący z kilkoma tabelami otrzymują wiele instrukcji przełączających, co nie wydaje się zbyt eleganckie. Ale rozumiem, że tak robi większość ludzi.
Gunnar Lium
Czy notifyChange naprawdę powinno używać _uri, a nie oryginalnego URI?
rozpiętość
18
Czy to przyjęty standard w systemie Android? To oczywiście działa, ale wydaje się trochę „niezgrabne”.
prolink007
Zawsze można użyć instrukcji switch jako swego rodzaju routera. Następnie podaj oddzielne metody obsługi każdego zasobu. query, queryUsers, queryUser, queryGroups, queryGroup To jest jak wbudowany w kontaktach dostawca robi. com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/...
Alex
2
Biorąc pod uwagę, że pytanie zawiera zalecenie dotyczące najlepszego projektu klasy bazy danych, dodałbym, że tabele powinny być zdefiniowane w ich własnej klasie, a członkowie klasy stanu ujawniają atrybuty, takie jak nazwa tabeli i kolumny.
MM.
10

Polecam sprawdzenie kodu źródłowego ContactProvider Androida 2.x. (Które można znaleźć w Internecie). Obsługują zapytania między tabelami, udostępniając wyspecjalizowane widoki, które następnie uruchamiają zapytania na zapleczu. Z przodu są dostępne dla dzwoniącego za pośrednictwem różnych identyfikatorów URI za pośrednictwem jednego dostawcy treści. Prawdopodobnie będziesz chciał również zapewnić klasę lub dwie do przechowywania stałych dla nazw pól tabeli i ciągów URI. Te klasy mogą być dostarczane jako dołączenie interfejsu API lub jako upuszczenie klasy, co znacznie ułatwi korzystanie z aplikacji.

Jest to trochę skomplikowane, więc możesz również chcieć sprawdzić, jak działa kalendarz, aby zorientować się, co robisz, a czego nie potrzebujesz.

Do wykonania większości prac potrzebny jest tylko jeden adapter DB i jeden dostawca treści na bazę danych (nie na tabelę), ale jeśli naprawdę chcesz, możesz użyć wielu adapterów / dostawców. To tylko trochę komplikuje sprawę.

Charles B.
źródło
5
com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/...
Alex
@Marloke Thanks. Ok, rozumiem, że nawet Android zespół używać switchrozwiązanie, ale ta część wspomniałeś: 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. Czy myślisz, że mógłbyś to trochę bardziej szczegółowo wyjaśnić?
eddy
7

Jeden ContentProvider mogą służyć wiele tabel, ale powinny być one nieco podobne. Będzie to miało znaczenie, jeśli zamierzasz zsynchronizować dostawców. Jeśli chcesz osobnych synchronizacji, powiedzmy, dla Kontaktów, Poczty lub Kalendarza, będziesz potrzebować różnych dostawców dla każdego z nich, nawet jeśli znajdą się w tej samej bazie danych lub są zsynchronizowani z tą samą usługą, ponieważ Adaptery synchronizacji są bezpośrednio powiązane z konkretnego dostawcy.

O ile wiem, możesz używać tylko jednego SQLiteOpenHelper na bazę danych, ponieważ przechowuje swoje metainformacje w tabeli w bazie danych. Więc jeśli masz ContentProvidersdostęp do tej samej bazy danych, będziesz musiał w jakiś sposób udostępnić pomocnika.

Timo Ohr
źródło
7

Uwaga: To jest wyjaśnienie / modyfikacja odpowiedzi udzielonej przez Opy.

Takie podejście dzieli każdy z insert, delete, update, i getTypemetody z oświadczeń przełącznika w celu obsługi każdego z poszczególnych tabel. Użyjesz CASE do zidentyfikowania każdej tabeli (lub uri), do której ma się odwoływać. Każdy CASE mapuje następnie do jednej z twoich tabel lub identyfikatorów URI. Np. TABLE1 lub URI1 jest wybrany w CASE # 1 itd. Dla wszystkich tabel używanych w aplikacji.

Oto przykład takiego podejścia. Dotyczy to metody wstawiania. Jest zaimplementowany nieco inaczej niż Opy, ale spełnia tę samą funkcję. Możesz wybrać preferowany styl. Chciałem też mieć pewność, że funkcja insert zwraca wartość, nawet jeśli wstawienie tabeli się nie powiedzie. W takim przypadku zwraca 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
źródło
3

Znalazłem najlepsze demo i wyjaśnienie dla ContentProvider i myślę, że jest zgodne ze standardami Androida.

Klasy kontraktowe

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

i klasy wewnętrzne:

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

Teraz tworzę bazę danych za pomocą 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 + "));"
        );
    }
}

Dostawca treści:

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

Mam nadzieję, że to ci pomoże.

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

Cały artykuł: https://guides.codepath.com/android/creating-content-providers

Bibliografia:

Uwaga: skopiowałem kod tylko dlatego, że link do dema lub artykułu może zostać w przyszłości usunięty.

Pratik Butani
źródło