Best Practices für die Bereitstellung mehrerer Tabellen mithilfe von Inhaltsanbietern in Android

90

Ich baue eine App, in der ich einen Tisch für Veranstaltungen und einen Tisch für Veranstaltungsorte habe. Ich möchte anderen Anwendungen Zugriff auf diese Daten gewähren können. Ich habe einige Fragen zu Best Practices für diese Art von Problem.

  1. Wie soll ich die Datenbankklassen strukturieren? Ich habe derzeit Klassen für EventsDbAdapter und VenuesDbAdapter, die die Logik zum Abfragen jeder Tabelle bereitstellen, während ich einen separaten DbManager (erweitert SQLiteOpenHelper) zum Verwalten von Datenbankversionen, Erstellen / Aktualisieren von Datenbanken und Ermöglichen des Zugriffs auf Datenbanken (getWriteable / ReadeableDatabase) habe. Ist dies die empfohlene Lösung, oder wäre es besser, wenn Sie entweder alles in einer Klasse (dh dem DbManager) konsolidieren oder alles trennen und jeden Adapter SQLiteOpenHelper erweitern lassen?

  2. Wie soll ich Inhaltsanbieter für mehrere Tabellen entwerfen? Sollte ich bei der vorherigen Frage einen Inhaltsanbieter für die gesamte App verwenden oder separate Anbieter für Veranstaltungen und Veranstaltungsorte erstellen?

Die meisten Beispiele, die ich finde, befassen sich nur mit Apps für einzelne Tabellen, daher würde ich mich über Hinweise hier freuen.

Gunnar Lium
quelle

Antworten:

114

Es ist wahrscheinlich ein bisschen spät für Sie, aber andere mögen dies nützlich finden.

Zuerst müssen Sie mehrere CONTENT_URIs erstellen

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

Dann erweitern Sie Ihren 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);      
}

Dann erstellen Sie Ihre Tabellen

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

Vergessen Sie nicht , den zweiten hinzufügen DATABASE_CREATEzuonCreate()

Sie werden einen Switch-Case- Block verwenden, um zu bestimmen, welche Tabelle verwendet wird. Dies ist mein Einfügecode

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

Sie müssen die devide up delete, update, getTypeetc. Überall dort , wo Ihr Provider fordert DATABASE_TABLE oder CONTENT_URI Sie einen Fall hinzufügen und haben DATABASE_TABLE1 oder CONTENT_URI1 in ein und # 2 in der nächsten und so weiter für so viele , wie Sie wollen.

Opy
quelle
1
Vielen Dank für Ihre Antwort, dies war ziemlich nah an der Lösung, die ich letztendlich verwendet habe. Ich finde, dass komplexe Anbieter, die mit mehreren Tabellen arbeiten, viele switch-Anweisungen erhalten, was nicht allzu elegant erscheint. Aber ich verstehe, dass die meisten Leute das so machen.
Gunnar Lium
Soll der notifyChange wirklich das _uri und nicht das ursprüngliche uri verwenden?
Spanne
18
Ist dies der akzeptierte Standard bei Android? Es funktioniert natürlich, scheint aber etwas "klobig" zu sein.
Prolink007
Kann die switch-Anweisungen immer nur als eine Art Router verwenden. Stellen Sie dann separate Methoden bereit, um jede Ressource zu bedienen. query, queryUsers, queryUser, queryGroups, queryGroup Das ist , wie die integrierten Kontakte Anbieter tun es. com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex
2
Angesichts der Tatsache, dass in der Frage eine Empfehlung für das Design von Best-Practice-Datenbankklassen angefordert wird, möchte ich hinzufügen, dass Tabellen in ihrer eigenen Klasse definiert werden sollten, wobei Mitglieder der Statusklasse Attribute wie Tabellen- und Spaltennamen offenlegen.
MM.
10

Ich empfehle, den Quellcode für den Android 2.x ContactProvider zu überprüfen. (Was online zu finden ist). Sie verarbeiten tabellenübergreifende Abfragen, indem sie spezielle Ansichten bereitstellen, für die Sie dann Abfragen im Back-End ausführen. Am Frontend sind sie für den Anrufer über verschiedene URIs über einen einzigen Inhaltsanbieter zugänglich. Möglicherweise möchten Sie auch eine oder zwei Klassen zum Halten von Konstanten für Ihre Tabellenfeldnamen und URI-Zeichenfolgen bereitstellen. Diese Klassen können entweder als API-Include oder als Drop-In-Klasse bereitgestellt werden und erleichtern der konsumierenden Anwendung die Verwendung erheblich.

Es ist ein bisschen komplex, deshalb möchten Sie vielleicht auch den Kalender überprüfen, um eine Vorstellung davon zu bekommen, was Sie tun und was nicht.

Sie sollten nur einen einzigen DB-Adapter und einen einzigen Inhaltsanbieter pro Datenbank (nicht pro Tabelle) benötigen, um den größten Teil der Arbeit zu erledigen. Sie können jedoch mehrere Adapter / Anbieter verwenden, wenn Sie dies wirklich möchten. Das macht die Sache nur etwas komplizierter.

Charles B.
quelle
5
com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex
@ Marloke Danke. Ok, ich verstehe, dass sogar das Android-Team die switchLösung verwendet, aber dieser Teil, den Sie erwähnt haben : 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. Glaubst du, du könntest das etwas ausführlicher erklären?
Wirbel
7

Man ContentProviderkann mehrere Tabellen bedienen, aber sie sollten etwas verwandt sein. Es macht einen Unterschied, ob Sie Ihre Anbieter synchronisieren möchten. Wenn Sie separate Synchronisierungen für beispielsweise Kontakte, E-Mail oder Kalender wünschen, benötigen Sie für jeden von ihnen unterschiedliche Anbieter, auch wenn sie sich in derselben Datenbank befinden oder mit demselben Dienst synchronisiert werden, da Synchronisierungsadapter direkt mit ihnen verknüpft sind ein bestimmter Anbieter.

Soweit ich das beurteilen kann, können Sie jedoch nur einen einzigen SQLiteOpenHelper pro Datenbank verwenden, da er seine Metainformationen in einer Tabelle in der Datenbank speichert. Wenn Sie also ContentProvidersauf dieselbe Datenbank zugreifen, müssen Sie den Helper irgendwie freigeben.

Timo Ohr
quelle
7

Hinweis: Dies ist eine Klarstellung / Änderung der Antwort von Opy.

Dieser Ansatz unterteilt jede der insert, delete, updateund getTypeMethoden mit switch - Anweisungen , um jede Ihrer einzelnen Tabellen zu behandeln. Sie verwenden einen CASE, um jede Tabelle (oder URL) zu identifizieren, auf die verwiesen werden soll. Jeder CASE wird dann einer Ihrer Tabellen oder URIs zugeordnet. Beispielsweise wird TABELLE1 oder URI1 in FALL 1 usw. für alle Tabellen ausgewählt, die Ihre App verwendet.

Hier ist ein Beispiel für den Ansatz. Dies ist für die Einfügemethode. Es ist etwas anders implementiert als Opy's, hat aber die gleiche Funktion. Sie können den gewünschten Stil auswählen. Ich wollte auch sicherstellen, dass insert einen Wert zurückgibt, auch wenn das Einfügen der Tabelle fehlschlägt. In diesem Fall wird a zurückgegeben -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
quelle
3

Ich habe die beste Demo und Erklärung für ContentProvider gefunden und denke, dass sie den Android-Standards entspricht.

Vertragsklassen

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

und innere Klassen:

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

Erstellen Sie nun eine Datenbank mit 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 + "));"
        );
    }
}

Inhalt Anbieter:

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

Ich hoffe es wird dir helfen.

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

Vollständiger Artikel: https://guides.codepath.com/android/creating-content-providers

Verweise:

Hinweis: Ich habe Code kopiert, nur weil der Link der Demo oder des Artikels in Zukunft möglicherweise entfernt wird.

Pratik Butani
quelle