Các phương pháp hay nhất để hiển thị nhiều bảng bằng cách sử dụng nhà cung cấp nội dung trong Android


90

Tôi đang xây dựng một ứng dụng nơi tôi có một bàn cho các sự kiện và một bàn cho các địa điểm. Tôi muốn có thể cấp cho các ứng dụng khác quyền truy cập vào dữ liệu này. Tôi có một số câu hỏi liên quan đến các phương pháp hay nhất cho loại vấn đề này.

  1. Tôi nên cấu trúc các lớp cơ sở dữ liệu như thế nào? Tôi hiện có các lớp cho EventsDbAdapter và VenuesDbAdapter, cung cấp logic để truy vấn từng bảng, trong khi có một DbManager riêng (mở rộng SQLiteOpenHelper) để quản lý các phiên bản cơ sở dữ liệu, tạo / nâng cấp cơ sở dữ liệu, cấp quyền truy cập vào cơ sở dữ liệu (getWritable / ReadableDatabase). Đây có phải là giải pháp được khuyến nghị hay tốt hơn là tôi nên hợp nhất mọi thứ vào một lớp (tức là DbManager) hoặc tách mọi thứ và để mỗi Bộ điều hợp mở rộng SQLiteOpenHelper?

  2. Tôi nên thiết kế nhà cung cấp nội dung cho nhiều bảng như thế nào? Mở rộng câu hỏi trước, tôi nên sử dụng một Nhà cung cấp nội dung cho toàn bộ ứng dụng hay tôi nên tạo các nhà cung cấp riêng biệt cho Sự kiện và Địa điểm?

Hầu hết các ví dụ tôi thấy chỉ giải quyết các ứng dụng bảng đơn lẻ, vì vậy tôi sẽ đánh giá cao bất kỳ gợi ý nào ở đây.

Câu trả lời:


114

Có thể là hơi muộn đối với bạn, nhưng những người khác có thể thấy điều này hữu ích.

Trước tiên, bạn cần tạo nhiều 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");

Sau đó, bạn mở rộng Trình kết hợp URI của mình

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

Sau đó, tạo bảng của bạn

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

Đừng quên thêm phần thứ hai DATABASE_CREATEvàoonCreate()

Bạn sẽ sử dụng một khối switch-case để xác định bảng nào được sử dụng. Đây là mã chèn của tôi

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

Bạn sẽ cần phải chia lên delete, update, getTypevv Bất cứ nơi nào gọi nhà cung cấp của bạn cho DATABASE_TABLE hoặc CONTENT_URI bạn sẽ thêm một trường hợp và có DATABASE_TABLE1 hoặc CONTENT_URI1 trong một và # 2 ở bên cạnh và vân vân cho càng nhiều như bạn muốn.


1
Cảm ơn câu trả lời của bạn, điều này khá gần với giải pháp tôi đã sử dụng. Tôi thấy rằng các nhà cung cấp phức tạp làm việc với một số bảng nhận được rất nhiều câu lệnh chuyển đổi, điều này có vẻ không thanh lịch cho lắm. Nhưng tôi hiểu đó là cách hầu hết mọi người làm.
Gunnar Lium

Có thực sự phải thông báo để sử dụng _uri chứ không phải là tiểu ban đầu?
span

18
Đây có phải là tiêu chuẩn được chấp nhận với Android không? Nó hoạt động, rõ ràng, nhưng có vẻ hơi "lắt léo".
prolink007

Luôn luôn có thể chỉ cần sử dụng các câu lệnh switch như một bộ định tuyến. Sau đó, cung cấp các phương thức riêng biệt để phục vụ từng tài nguyên. query, queryUsers, queryUser, queryGroups, queryGroup Đây là cách tích hợp trong danh bạ nhà cung cấp thực hiện nó. com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex

2
Do câu hỏi yêu cầu đề xuất cho thiết kế lớp cơ sở dữ liệu thực tiễn tốt nhất, tôi sẽ thêm rằng các bảng nên được định nghĩa trong lớp riêng của chúng, với các thành viên lớp trạng thái hiển thị các thuộc tính như tên bảng và cột.
MM.

10

Tôi khuyên bạn nên kiểm tra mã nguồn cho Trình cung cấp dịch vụ liên hệ Android 2.x. (Có thể tìm thấy trên mạng). Chúng xử lý các truy vấn bảng chéo bằng cách cung cấp các chế độ xem chuyên biệt mà sau đó bạn chạy các truy vấn trên back end. Ở mặt trước, người gọi có thể truy cập chúng qua nhiều URI khác nhau thông qua một nhà cung cấp nội dung duy nhất. Có thể bạn cũng sẽ muốn cung cấp một hoặc hai lớp để giữ hằng số cho tên trường bảng và chuỗi URI của mình. Các lớp này có thể được cung cấp dưới dạng API bao gồm hoặc dưới dạng lớp giảm và sẽ giúp ứng dụng sử dụng dễ dàng hơn nhiều.

Nó hơi phức tạp, vì vậy bạn cũng có thể muốn xem lịch như thế nào để có ý tưởng về những gì bạn làm và không cần.

Bạn chỉ cần một bộ điều hợp DB duy nhất và một nhà cung cấp Nội dung duy nhất trên mỗi cơ sở dữ liệu (không phải trên mỗi bảng) để thực hiện hầu hết công việc, nhưng bạn có thể sử dụng nhiều bộ điều hợp / nhà cung cấp nếu bạn thực sự muốn. Nó chỉ làm cho mọi thứ phức tạp hơn một chút.


5
com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex

@Marloke Cảm ơn. Ok, tôi hiểu rằng ngay cả những đội Android sử dụng các switchgiải pháp, nhưng phần này, bạn đề cập: 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. Bạn có nghĩ rằng bạn có thể giải thích điều đó chi tiết hơn một chút không?
xoáy

7

Một người ContentProvidercó thể phục vụ nhiều bảng, nhưng chúng phải có liên quan. Nó sẽ tạo ra sự khác biệt nếu bạn có ý định đồng bộ hóa các nhà cung cấp của mình. Nếu bạn muốn đồng bộ hóa riêng biệt cho, giả sử Danh bạ, Thư hoặc Lịch, bạn sẽ cần các nhà cung cấp khác nhau cho từng người trong số họ, ngay cả khi chúng nằm trong cùng một cơ sở dữ liệu hoặc được đồng bộ hóa với cùng một dịch vụ, vì Bộ điều hợp đồng bộ hóa được liên kết trực tiếp với một nhà cung cấp cụ thể.

Theo như tôi có thể nói, bạn chỉ có thể sử dụng một SQLiteOpenHelper duy nhất cho mỗi cơ sở dữ liệu, vì nó lưu trữ thông tin meta của nó trong một bảng trong cơ sở dữ liệu. Vì vậy, nếu bạn ContentProviderstruy cập cùng một cơ sở dữ liệu, bằng cách nào đó bạn sẽ phải chia sẻ Người trợ giúp.


7

Lưu ý: Đây là phần làm rõ / sửa đổi câu trả lời do Opy cung cấp.

Cách tiếp cận này subdivides mỗi insert, delete, update, và getTypephương pháp với báo cáo công tắc để xử lý mỗi bảng cá nhân của bạn. Bạn sẽ sử dụng CASE để xác định từng bảng (hoặc uri) sẽ được tham chiếu. Sau đó, mỗi CASE ánh xạ tới một trong các bảng hoặc URI của bạn. Ví dụ: TABLE1 hoặc URI1 được chọn trong CASE # 1, v.v. cho tất cả các bảng mà ứng dụng của bạn sử dụng.

Đây là một ví dụ về cách tiếp cận. Đây là cho phương pháp chèn. Nó được triển khai hơi khác với Opy's nhưng thực hiện cùng một chức năng. Bạn có thể chọn phong cách bạn thích. Tôi cũng muốn chắc chắn rằng chèn trả về một giá trị ngay cả khi việc chèn bảng không thành công. Trong trường hợp đó, nó trả về a -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]

3

Tôi đã tìm thấy bản giới thiệu và giải thích tốt nhất cho ContentProvider và tôi nghĩ rằng nó đã tuân theo Tiêu chuẩn Android.

Các lớp hợp đồng

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

Lớp bên trong:

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

Bây giờ tạo Cơ sở dữ liệu bằng 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 + "));"
        );
    }
}

Nhà cung cấp nội dung:

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

Tôi hy vọng nó sẽ giúp bạn.

Demo trên GitHub: https://github.com/androidessence/MovieDatabase

Bài viết đầy đủ: https://guides.codepath.com/android/creating-content-providers

Người giới thiệu:

Lưu ý: Tôi đã sao chép mã chỉ vì nếu liên kết của bản demo hoặc bài viết có thể bị xóa trong tương lai.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.