Prácticas recomendadas para exponer varias tablas mediante proveedores de contenido en Android

90

Estoy creando una aplicación en la que tengo una mesa para eventos y una mesa para lugares. Quiero poder otorgar acceso a estos datos a otras aplicaciones. Tengo algunas preguntas relacionadas con las mejores prácticas para este tipo de problema.

  1. ¿Cómo debo estructurar las clases de la base de datos? Actualmente tengo clases para EventsDbAdapter y VenuesDbAdapter, que proporcionan la lógica para consultar cada tabla, mientras que tengo un DbManager separado (extiende SQLiteOpenHelper) para administrar versiones de bases de datos, crear / actualizar bases de datos, dar acceso a la base de datos (getWriteable / ReadeableDatabase). ¿Es esta la solución recomendada, o sería mejor consolidar todo en una clase (es decir, el DbManager) o separar todo y dejar que cada adaptador extienda SQLiteOpenHelper?

  2. ¿Cómo debo diseñar proveedores de contenido para varias tablas? Ampliando la pregunta anterior, ¿debo usar un proveedor de contenido para toda la aplicación o debo crear proveedores separados para eventos y lugares?

La mayoría de los ejemplos que encuentro solo tratan con aplicaciones de una sola tabla, por lo que agradecería cualquier sugerencia aquí.

Gunnar Lium
fuente

Respuestas:

114

Probablemente sea un poco tarde para ti, pero otros pueden encontrar esto útil.

Primero debes crear varios 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");

Luego expande su 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);      
}

Entonces crea tus tablas

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

No olvide agregar el segundo DATABASE_CREATEaonCreate()

Va a utilizar un bloque de caso de interruptor para determinar qué tabla se utiliza. Este es mi código de inserción

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

Tendrá que dividiremos el delete, update, getType, etc. Dondequiera que sus llamadas del proveedor de DATABASE_TABLE o CONTENT_URI va a agregar un caso y tienen DATABASE_TABLE1 o CONTENT_URI1 en uno y # 2 en el siguiente y así sucesivamente para todas las que quieran.

Opy
fuente
1
Gracias por tu respuesta, esto se acercó bastante a la solución que terminé usando. Encuentro que los proveedores complejos que trabajan con varias tablas obtienen muchas declaraciones de cambio, lo que no parece tan elegante. Pero entiendo que esa es la forma en que la mayoría de la gente lo hace.
Gunnar Lium
¿Se supone que el notifyChange usa el _uri y no el uri original?
abarcar el
18
¿Es este el estándar aceptado con Android? Funciona, obviamente, pero parece un poco "torpe".
prolink007
Siempre se pueden usar las declaraciones del interruptor como una especie de enrutador. Luego proporcione métodos separados para servir cada recurso. query, queryUsers, queryUser, queryGroups, queryGroup Así es como el proveedor de contactos incorporados hace. com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex
2
Dado que la pregunta solicita una recomendación para el diseño de la clase de base de datos de mejores prácticas, agregaría que las tablas deben definirse en su propia clase, con los miembros de la clase estatal exponiendo atributos como el nombre de la tabla y la columna.
MM.
10

Recomiendo consultar el código fuente de ContactProvider de Android 2.x. (Que se puede encontrar en línea). Manejan consultas de tabla cruzada al proporcionar vistas especializadas que luego ejecuta consultas en el back-end. En la interfaz, la persona que llama puede acceder a ellos a través de varios URI diferentes a través de un único proveedor de contenido. Probablemente también querrá proporcionar una clase o dos para mantener constantes para los nombres de campo de su tabla y cadenas de URI. Estas clases se pueden proporcionar como una API incluida o como una caída en la clase, y harán que sea mucho más fácil de usar para la aplicación consumidora.

Es un poco complejo, por lo que es posible que también desee ver cómo funciona el calendario para tener una idea de lo que necesita y lo que no necesita.

Solo debería necesitar un único adaptador de base de datos y un único proveedor de contenido por base de datos (no por tabla) para realizar la mayor parte del trabajo, pero puede utilizar varios adaptadores / proveedores si realmente lo desea. Simplemente complica un poco las cosas.

Carlos B
fuente
5
com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex
@Marloke Gracias. Ok, entiendo que incluso el equipo de Android utilizan la switchsolución, pero esta parte que mencioné: 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. ¿Crees que podrías explicarlo un poco más en detalle?
eddy
7

Uno ContentProvider pueden servir varias mesas, pero deberían estar algo relacionadas. Marcará la diferencia si tiene la intención de sincronizar sus proveedores. Si desea sincronizaciones separadas para, digamos, Contactos, Correo o Calendario, necesitará diferentes proveedores para cada uno de ellos, incluso si terminan en la misma base de datos o están sincronizados con el mismo servicio, porque los Adaptadores de sincronización están vinculados directamente a un proveedor en particular.

Sin embargo, por lo que puedo decir, solo puede usar un único SQLiteOpenHelper por base de datos, ya que almacena su metainformación en una tabla dentro de la base de datos. Entonces, si ContentProvidersaccede a la misma base de datos, tendrá que compartir el Asistente de alguna manera.

Timo Ohr
fuente
7

Nota: Esta es una aclaración / modificación a la respuesta proporcionada por Opy.

Este enfoque se subdivide cada uno de los insert, delete, update, y getTypemétodos con las instrucciones switch para manejar cada una de las tablas individuales. Utilizará CASE para identificar cada tabla (o uri) a la que se hará referencia. Luego, cada CASO se asigna a una de sus tablas o URI. Por ejemplo, TABLE1 o URI1 se selecciona en CASO # 1, etc. para todas las tablas que emplea su aplicación.

Aquí tienes un ejemplo del enfoque. Esto es para el método de inserción. Está implementado de manera un poco diferente a la de Opy, pero realiza la misma función. Puede seleccionar el estilo que prefiera. También quería estar seguro de que insertar devuelve un valor incluso si falla la inserción de la tabla. En ese caso, devuelve 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
fuente
3

Encontré la mejor demostración y explicación para ContentProvider y creo que ha seguido los estándares de Android.

Clases de contrato

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

y clases internas:

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

Ahora creando una base de datos usando 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 + "));"
        );
    }
}

Proveedor de contenido:

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

Espero que te ayude.

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

Articulo completo : https://guides.codepath.com/android/creating-content-providers

Referencias:

Nota: Copié el código solo porque si el enlace de la demostración o el artículo puede eliminarse en el futuro.

Pratik Butani
fuente