Práticas recomendadas para expor várias tabelas usando provedores de conteúdo no Android

90

Estou construindo um aplicativo onde tenho uma mesa para eventos e uma mesa para locais. Desejo poder conceder a outros aplicativos acesso a esses dados. Tenho algumas perguntas relacionadas às melhores práticas para esse tipo de problema.

  1. Como devo estruturar as classes de banco de dados? Atualmente, tenho classes para EventsDbAdapter e VenuesDbAdapter, que fornecem a lógica para consultar cada tabela, enquanto tenho um DbManager separado (estende SQLiteOpenHelper) para gerenciar versões de banco de dados, criar / atualizar bancos de dados, dar acesso ao banco de dados (getWriteable / ReadeableDatabase). Esta é a solução recomendada ou seria melhor consolidar tudo em uma classe (ou seja, o DbManager) ou separar tudo e deixar que cada adaptador estenda SQLiteOpenHelper?

  2. Como devo projetar provedores de conteúdo para várias tabelas? Estendendo a pergunta anterior, devo usar um Provedor de Conteúdo para todo o aplicativo ou devo criar provedores separados para Eventos e Locais?

A maioria dos exemplos que encontro lidam apenas com aplicativos de tabela única, portanto, gostaria de receber qualquer sugestão aqui.

Gunnar Lium
fonte

Respostas:

114

Provavelmente é um pouco tarde para você, mas outros podem achar isso útil.

Primeiro você precisa criar vários CONTENT_URIs

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

Então você expande seu 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);      
}

Em seguida, crie suas tabelas

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ão se esqueça de adicionar o segundo DATABASE_CREATEaonCreate()

Você usará um bloco de caixa de chave para determinar qual tabela é usada. Este é o meu código de inserção

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

Você terá que dividir o delete, update, getType, etc. Onde quer que suas chamadas de provedor para DATABASE_TABLE ou CONTENT_URI você irá adicionar um caso e têm DATABASE_TABLE1 ou CONTENT_URI1 em um e # 2 na próxima e assim por diante para tantos como você quer.

Opy
fonte
1
Obrigado pela sua resposta, esta foi muito próxima da solução que acabei usando. Acho que provedores complexos que trabalham com várias tabelas obtêm muitas instruções switch, o que não parece muito elegante. Mas eu entendo que é assim que a maioria das pessoas faz.
Gunnar Lium
O notificChange realmente deveria usar o _uri e não o uri original?
período de
18
Este é o padrão aceito no Android? Funciona, obviamente, mas parece um pouco "desajeitado".
prolink007
Sempre pode usar as instruções switch como uma espécie de roteador. Em seguida, forneça métodos separados para servir cada recurso. query, queryUsers, queryUser, queryGroups, queryGroup Isto é como o provedor de built-in contatos faz. com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex
2
Dado que a pergunta solicita recomendação para o design de classe de banco de dados de prática recomendada, eu acrescentaria que as tabelas devem ser definidas em sua própria classe, com os membros da classe de estado expondo atributos como tabela e nome de coluna.
MM.
10

Recomendo verificar o código-fonte do ContactProvider do Android 2.x. (Que pode ser encontrado online). Eles lidam com consultas de tabela cruzada, fornecendo visualizações especializadas nas quais você executa consultas no back end. No front-end, eles são acessíveis ao chamador por meio de vários URIs diferentes por meio de um único provedor de conteúdo. Provavelmente, você também desejará fornecer uma ou duas classes para manter constantes para os nomes dos campos da tabela e strings de URI. Essas classes podem ser fornecidas como uma inclusão de API ou como uma queda na classe e tornarão muito mais fácil para o aplicativo de consumo usar.

É um pouco complexo, então você também pode querer verificar como funciona o calendário para ter uma ideia do que você faz e do que não precisa.

Você deve precisar apenas de um único adaptador de banco de dados e um único provedor de conteúdo por banco de dados (não por tabela) para fazer a maior parte do trabalho, mas você pode usar vários adaptadores / provedores se realmente quiser. Isso só torna as coisas um pouco mais complicadas.

Charles B
fonte
5
com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex
@Marloke Obrigado. Ok, eu entendo que até mesmo a equipe do Android usar a switchsolução, mas esta parte que você mencionou: 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. Você acha que poderia explicar isso um pouco mais detalhadamente?
redemoinho
7

Um ContentProviderpode servir várias tabelas, mas eles devem ser algo relacionado. Vai fazer diferença se você pretende sincronizar seus provedores. Se você quiser sincronizações separadas para, digamos, Contatos, Correio ou Calendário, você precisará de provedores diferentes para cada um deles, mesmo que eles acabem no mesmo banco de dados ou sejam sincronizados com o mesmo serviço, porque os Adaptadores de Sincronização estão ligados diretamente a um provedor específico.

Até onde eu posso dizer, você só pode usar um único SQLiteOpenHelper por banco de dados, já que ele armazena suas metainformações em uma tabela dentro do banco de dados. Portanto, se você ContentProvidersacessar o mesmo banco de dados, precisará compartilhar o Helper de algum modo.

Timo Ohr
fonte
7

Observação: este é um esclarecimento / modificação à resposta fornecida pela Opy.

Esta abordagem subdivide cada um dos insert, delete, update, e getTypemétodos com declarações switch para lidar com cada uma de suas tabelas individuais. Você usará um CASE para identificar cada tabela (ou uri) a ser referenciada. Cada CASE então mapeia para uma de suas tabelas ou URIs. Por exemplo, TABLE1 ou URI1 é selecionado no CASO # 1, etc. para todas as tabelas que seu aplicativo utiliza.

Aqui está um exemplo da abordagem. Isso é para o método de inserção. É implementado um pouco diferente do Opy, mas executa a mesma função. Você pode selecionar o estilo de sua preferência. Eu também queria ter certeza de que insert retornaria um valor mesmo se a inserção da tabela falhar. Nesse caso, ele retorna um -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
fonte
3

Encontrei a melhor demonstração e explicação para ContentProvider e acho que segue os padrões do Android.

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

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

Agora criando banco de dados 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 + "));"
        );
    }
}

Provedor de conteúdos:

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 ajude você.

Demonstração no GitHub: https://github.com/androidessence/MovieDatabase

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

Referências:

Nota: Copiei o código apenas porque se o link da demonstração ou artigo puder ser removido no futuro.

Pratik Butani
fonte