【问题标题】:Best practices for exposing multiple tables using content providers in Android在 Android 中使用内容提供程序公开多个表的最佳实践
【发布时间】:2011-04-18 08:37:13
【问题描述】:

我正在构建一个应用程序,其中有一张用于活动的桌子和一张用于场地的桌子。我希望能够授予其他应用程序访问此数据的权限。我有几个与此类问题的最佳实践相关的问题。

  1. 我应该如何构建数据库类? 我目前有 EventsDbAdapter 和 VenuesDbAdapter 的类,它们提供查询每个表的逻辑,同时有一个单独的 DbManager(扩展 SQLiteOpenHelper)来管理数据库版本、创建/升级数据库、访问数据库(getWriteable/ReadeableDatabase)。这是推荐的解决方案,还是将所有内容整合到一个类(即 DbManager)或分离所有内容并让每个适配器扩展 SQLiteOpenHelper 会更好?

  2. 我应该如何为多个表设计内容提供者? 扩展上一个问题,我应该为整个应用使用一个 Content Provider,还是应该为 Events 和 Venues 创建单独的提供程序?

我发现的大多数示例只处理单表应用程序,因此我希望您能在此处提供任何指点。

【问题讨论】:

    标签: android sqlite android-contentprovider


    【解决方案1】:

    对您来说可能有点晚了,但其他人可能会觉得这很有用。

    首先你需要创建多个 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");
    

    然后你扩展你的 URI 匹配器

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

    然后创建你的表

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

    别忘了将第二个DATABASE_CREATE 添加到onCreate()

    您将使用 switch-case 块来确定使用哪个表。这是我的插入代码

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

    您将需要划分deleteupdategetType 等。无论您的提供者在何处调用 DATABASE_TABLE 或 CONTENT_URI,您都将添加一个案例并在一个中包含 DATABASE_TABLE1 或 CONTENT_URI1,在下一个中包含 #2等等。

    【讨论】:

    • 感谢您的回答,这与我最终使用的解决方案非常接近。我发现使用多个表的复杂提供程序会得到很多 switch 语句,这看起来并不那么优雅。但我知道大多数人都是这样做的。
    • notifyChange 真的应该使用 _uri 而不是原始 uri 吗?
    • 这是 Android 公认的标准吗?显然,它有效,但似乎有点“笨拙”。
    • 总是可以将 switch 语句用作某种路由器。然后提供单独的方法来服务每个资源。 queryqueryUsersqueryUserqueryGroupsqueryGroup 这就是内置联系人提供程序的工作方式。 com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
    • 鉴于该问题要求推荐最佳实践数据库类设计,我要补充一点,表应在自己的类中定义,状态类成员公开表和列名等属性。跨度>
    【解决方案2】:

    我建议查看 Android 2.x ContactProvider 的源代码。 (可以在网上找到)。它们通过提供专门的视图来处理跨表查询,然后您可以在后端运行查询。在前端,调用者可以通过单个内容提供者通过各种不同的 URI 访问它们。您可能还需要提供一两个类来保存表字段名称和 URI 字符串的常量。这些类可以作为 API 包含或作为类的一部分提供,这将使消费应用程序更容易使用。

    它有点复杂,因此您可能还想查看日历如何了解您需要做什么和不需要做什么。

    每个数据库(而不是每个表)只需要一个 DB 适配器和一个 Content Provider 即可完成大部分工作,但如果您确实需要,也可以使用多个适配器/提供程序。它只会让事情变得更复杂。

    【讨论】:

    • com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
    • @Marloke 谢谢。好的,我知道即使是 Android 团队也使用switch 解决方案,但是您提到的这一部分: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。你认为你可以更详细地解释一下吗?
    【解决方案3】:

    一个ContentProvider 可以为多个表提供服务,但它们应该有些相关。如果您打算同步您的供应商,那将会有所作为。如果您想要单独同步,例如联系人、邮件或日历,您将需要为它们中的每一个提供不同的提供程序,即使它们最终位于同一个数据库中或与同一个服务同步,因为同步适配器直接绑定到特定的提供者。

    据我所知,每个数据库只能使用一个 SQLiteOpenHelper,因为它将元信息存储在数据库内的表中。因此,如果您的 ContentProviders 访问同一个数据库,您将不得不以某种方式共享 Helper。

    【讨论】:

      【解决方案4】:

      注意:这是对 Opy 提供的答案的澄清/修改。

      此方法使用 switch 语句细分 insertdeleteupdategetType 方法,以便处理每个单独的表。您将使用 CASE 来标识要引用的每个表(或 uri)。然后每个 CASE 映射到您的表或 URI 之一。例如,TABLE1URI1 在 CASE #1 等中为您的应用使用的所有表选择。

      这是该方法的一个示例。这是用于插入方法。它的实现与 Opy 的略有不同,但执行相同的功能。您可以选择您喜欢的样式。即使表插入失败,我也想确保 insert 返回一个值。在这种情况下,它返回一个-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]
      

      【讨论】:

        【解决方案5】:

        我找到了 ContentProvider 的最佳演示和解释,我认为它符合 Android 标准。

        合同类

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

        内部类:

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

        现在使用 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 + "));"
                );
            }
        }
        

        内容提供者:

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

        希望对你有帮助。

        GitHub 上的演示:https://github.com/androidessence/MovieDatabase

        全文:https://guides.codepath.com/android/creating-content-providers

        参考资料:

        注意:我复制代码只是因为将来可能会删除演示或文章的链接。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-12-02
          • 2014-05-02
          相关资源
          最近更新 更多