Meilleures pratiques pour exposer plusieurs tables à l'aide de fournisseurs de contenu dans Android

90

Je construis une application où j'ai une table pour les événements et une table pour les lieux. Je souhaite pouvoir autoriser d'autres applications à accéder à ces données. J'ai quelques questions relatives aux meilleures pratiques pour ce genre de problème.

  1. Comment dois-je structurer les classes de base de données? J'ai actuellement des classes pour EventsDbAdapter et VenuesDbAdapter, qui fournissent la logique d'interrogation de chaque table, tout en ayant un DbManager séparé (étend SQLiteOpenHelper) pour gérer les versions de base de données, créer / mettre à niveau les bases de données, donnant accès à la base de données (getWriteable / ReadeableDatabase). Est-ce la solution recommandée, ou est-ce que je ferais mieux de tout consolider dans une seule classe (c'est-à-dire le DbManager) ou de tout séparer et de laisser chaque adaptateur étendre SQLiteOpenHelper?

  2. Comment dois-je concevoir des fournisseurs de contenu pour plusieurs tables? En prolongeant la question précédente, dois-je utiliser un seul fournisseur de contenu pour l'ensemble de l'application ou dois-je créer des fournisseurs distincts pour les événements et les lieux?

La plupart des exemples que je trouve ne concernent que des applications à table unique, donc j'apprécierais tous les pointeurs ici.

Gunnar Lium
la source

Réponses:

114

Il est probablement un peu tard pour vous, mais d'autres peuvent trouver cela utile.

Vous devez d'abord créer plusieurs 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");

Ensuite, vous développez votre 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);      
}

Puis créez vos tableaux

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

N'oubliez pas d'ajouter le second DATABASE_CREATEàonCreate()

Vous allez utiliser un bloc de boîtier de commutation pour déterminer quelle table est utilisée. Ceci est mon code d'insertion

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

Vous devrez Devide le delete, update, getType, etc. Chaque fois que vos appels fournisseur pour DATABASE_TABLE ou CONTENT_URI vous ajouterez un cas et ont DATABASE_TABLE1 ou CONTENT_URI1 dans une seule et n ° 2 dans la prochaine et ainsi de suite pour autant que vous le souhaitez.

Opy
la source
1
Merci pour votre réponse, c'était assez proche de la solution que j'ai fini par utiliser. Je trouve que les fournisseurs complexes travaillant avec plusieurs tables reçoivent beaucoup d'instructions de commutation, ce qui ne semble pas si élégant. Mais je comprends que c'est ainsi que la plupart des gens le font.
Gunnar Lium
Le notifyChange est-il vraiment censé utiliser le _uri et non l'URI d'origine?
span
18
Est-ce la norme acceptée avec Android? Cela fonctionne, évidemment, mais semble un peu "maladroit".
prolink007
Peut toujours simplement utiliser les instructions switch comme une sorte de routeur. Ensuite, fournissez des méthodes distinctes pour servir chaque ressource. query, queryUsers, queryUser, queryGroups, queryGroup Voici comment le fournisseur intégré contacts fait. com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex
2
Étant donné que la question demande une recommandation pour la conception de classe de base de données des meilleures pratiques, j'ajouterais que les tables doivent être définies dans leur propre classe, les membres de la classe d'état exposant des attributs tels que le nom de la table et de la colonne.
MM.
10

Je recommande de consulter le code source du fournisseur de contact Android 2.x. (Qui peut être trouvé en ligne). Ils gèrent les requêtes entre tables en fournissant des vues spécialisées sur lesquelles vous exécutez ensuite des requêtes sur le serveur principal. Sur le front-end, ils sont accessibles à l'appelant via différents URI différents via un seul fournisseur de contenu. Vous voudrez probablement également fournir une classe ou deux pour contenir des constantes pour vos noms de champs de table et vos chaînes URI. Ces classes peuvent être fournies soit en tant qu'inclusion API, soit en tant que classe supplémentaire, et faciliteront considérablement l'utilisation de l'application consommatrice.

C'est un peu complexe, vous voudrez peut-être également vérifier comment le calendrier vous permet de vous faire une idée de ce que vous faites et de ce dont vous n'avez pas besoin.

Vous ne devriez avoir besoin que d'un seul adaptateur de base de données et d'un seul fournisseur de contenu par base de données (et non par table) pour effectuer la plupart du travail, mais vous pouvez utiliser plusieurs adaptateurs / fournisseurs si vous le souhaitez vraiment. Cela rend les choses un peu plus compliquées.

Charles B
la source
5
com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex
@Marloke Merci. Ok, je comprends que même l'équipe Android utilise la switchsolution, mais cette partie vous dit: 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. Pensez-vous pouvoir expliquer cela un peu plus en détail?
eddy
7

Une ContentProvider peut servir plusieurs tables, mais elles devraient être quelque peu liées. Cela fera une différence si vous avez l'intention de synchroniser vos fournisseurs. Si vous voulez des synchronisations séparées pour, disons Contacts, Mail ou Calendrier, vous aurez besoin de différents fournisseurs pour chacun d'eux, même s'ils finissent par être dans la même base de données ou sont synchronisés avec le même service, car les adaptateurs de synchronisation sont directement liés à un fournisseur particulier.

Pour autant que je sache, vous ne pouvez utiliser qu'un seul SQLiteOpenHelper par base de données, car il stocke ses méta-informations dans une table de la base de données. Donc, si vous ContentProvidersaccédez à la même base de données, vous devrez partager le Helper quelque part.

Timo Ohr
la source
7

Remarque: Ceci est une clarification / modification de la réponse fournie par Opy.

Cette approche subdivisant chacun des insert, delete, update, et les getTypeméthodes avec les déclarations de commutation afin de traiter chacun de vos tables individuelles. Vous utiliserez un CASE pour identifier chaque table (ou uri) à référencer. Chaque CASE correspond ensuite à l'une de vos tables ou URI. Par exemple, TABLE1 ou URI1 est sélectionné dans CAS N ° 1, etc. pour toutes les tables que votre application utilise.

Voici un exemple de l'approche. Ceci est pour la méthode d'insertion. Il est implémenté un peu différemment d'Opy mais remplit la même fonction. Vous pouvez sélectionner le style que vous préférez. Je voulais également m'assurer que l'insertion renvoie une valeur même si l'insertion de la table échoue. Dans ce cas, il renvoie un -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
la source
3

J'ai trouvé la meilleure démo et explication pour ContentProvider et je pense qu'il a suivi les normes Android.

Classes de contrat

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

et classes intérieures:

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

Création de la base de données en utilisant 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 + "));"
        );
    }
}

Fournisseur de contenu:

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

J'espère que cela vous aidera.

Démo sur GitHub: https://github.com/androidessence/MovieDatabase

Article complet: https://guides.codepath.com/android/creating-content-providers

Références:

Remarque: j'ai copié du code simplement parce que si le lien de la démo ou de l'article peut être supprimé à l'avenir.

Pratik Butani
la source