Android에서 콘텐츠 제공 업체를 사용하여 여러 테이블을 노출하는 모범 사례


90

이벤트 테이블과 장소 테이블이있는 앱을 만들고 있습니다. 다른 애플리케이션에이 데이터에 대한 액세스 권한을 부여하고 싶습니다. 이런 종류의 문제에 대한 모범 사례와 관련된 몇 가지 질문이 있습니다.

  1. 데이터베이스 클래스를 어떻게 구성해야합니까? 현재 각 테이블을 쿼리하는 논리를 제공하는 EventsDbAdapter 및 VenuesDbAdapter 클래스가 있으며 데이터베이스 버전 관리, 데이터베이스 생성 / 업그레이드, 데이터베이스 (getWriteable / ReadeableDatabase)에 대한 액세스를 제공하는 별도의 DbManager (SQLiteOpenHelper 확장)가 있습니다. 이것이 권장되는 솔루션입니까, 아니면 모든 것을 하나의 클래스 (예 : DbManager)로 통합하거나 모든 것을 분리하고 각 어댑터가 SQLiteOpenHelper를 확장하도록하는 것이 더 낫습니까?

  2. 여러 테이블에 대한 콘텐츠 공급자를 어떻게 디자인해야합니까? 이전 질문을 확장하면 전체 앱에 대해 하나의 콘텐츠 제공 업체를 사용해야합니까, 아니면 이벤트 및 장소에 대해 별도의 제공 업체를 만들어야합니까?

내가 찾은 대부분의 예제는 단일 테이블 앱만 다루기 때문에 여기에 대한 조언을 주시면 감사하겠습니다.

답변:


114

아마 조금 늦었지만 다른 사람들은 이것이 유용하다고 생각할 수 있습니다.

먼저 여러 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");

그런 다음 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);      
}

그런 다음 테이블을 만듭니다.

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()

당신은 사용하고자하는 스위치의 경우 사용되는지 테이블 결정하기 위해 블록을. 이것은 내 삽입 코드입니다.

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

당신은 위쪽으로 나눔해야합니다 delete, update, getType, 등 당신이 많은하는 한 등의 다음에 # 2 케이스를 추가하고이 DATABASE_TABLE1 또는 CONTENT_URI1을 한 것이다 DATABASE_TABLE 또는 CONTENT_URI을 위해 제공 호출 당신이 원하는 어디든지.


1
귀하의 답변에 감사드립니다. 이것은 내가 사용한 솔루션에 매우 가깝습니다. 여러 테이블로 작업하는 복잡한 공급자가 많은 switch-statement를 얻는다는 것을 알았습니다. 그러나 나는 그것이 대부분의 사람들이하는 방식임을 이해합니다.
Gunnar Lium

notifyChange는 실제로 원래 uri가 아닌 _uri를 사용해야합니까?
스팬

18
이것이 Android에서 허용되는 표준입니까? 분명히 작동하지만 약간 "투박"해 보입니다.
prolink007

항상 switch 문을 일종의 라우터로 사용할 수 있습니다. 그런 다음 각 리소스를 제공하는 별도의 방법을 제공합니다. query, queryUsers, queryUser, queryGroups, queryGroup 이 내장 된 연락처 제공 업체에서 수행하는 방법이다. com.android.providers.contacts.ContactsProvider2.java github.com/android/platform_packages_providers_contactsprovider/…
Alex

2
질문이 모범 사례 데이터베이스 클래스 디자인에 대한 권장 사항을 요청한다는 점을 감안할 때 상태 클래스 구성원이 테이블 및 열 이름과 같은 속성을 노출하는 상태로 테이블을 자체 클래스에 정의해야한다고 추가합니다.
MM.

10

Android 2.x ContactProvider의 소스 코드를 확인하는 것이 좋습니다. (온라인에서 찾을 수 있음). 이들은 백엔드에서 쿼리를 실행하는 특수 뷰를 제공하여 크로스 테이블 쿼리를 처리합니다. 프런트 엔드에서는 단일 콘텐츠 공급자를 통해 다양한 URI를 통해 호출자가 액세스 할 수 있습니다. 테이블 필드 이름과 URI 문자열에 대한 상수를 유지하기 위해 클래스를 한두 개 제공 할 수도 있습니다. 이러한 클래스는 API 포함 또는 클래스 드롭으로 제공 될 수 있으며 사용하는 애플리케이션이 훨씬 쉽게 사용할 수 있습니다.

조금 복잡하므로 캘린더를 확인하여 무엇을하고 필요하지 않은지에 대한 아이디어를 얻고 싶을 수도 있습니다.

대부분의 작업을 수행하려면 데이터베이스 당 단일 DB 어댑터와 단일 콘텐츠 공급자 (테이블 당 아님) 만 필요하지만 실제로 원하는 경우 여러 어댑터 / 공급자를 사용할 수 있습니다. 그것은 단지 일을 조금 더 복잡하게 만듭니다.


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

@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. 좀 더 자세히 설명해 주시겠습니까?
eddy

7

하나 ContentProvider 는 여러 테이블을 제공 할 수 있지만 다소 관련이 있어야합니다. 공급자를 동기화하려는 경우 차이가 있습니다. 별도의 동기화 (예 : 연락처, 메일 또는 캘린더)를 원하는 경우 동기화 어댑터가 직접 연결되어 있기 때문에 동일한 데이터베이스에 있거나 동일한 서비스와 동기화 되더라도 각각에 대해 다른 공급자가 필요합니다. 특정 공급자.

내가 말할 수있는 한, 데이터베이스 내의 테이블에 메타 정보를 저장하기 때문에 데이터베이스 당 하나의 SQLiteOpenHelper 만 사용할 수 있습니다. 따라서 ContentProviders동일한 데이터베이스에 액세스하는 경우 어떻게 든 도우미를 공유해야합니다.


7

참고 : 이것은 Opy가 제공 한 답변에 대한 설명 / 수정입니다.

이러한 접근 방식은 각각의 세분화 insert, delete, update, 및 getType개별 각 테이블을 처리하기 위해 switch 문에 방법. CASE를 사용하여 참조 할 각 테이블 (또는 uri)을 식별합니다. 그런 다음 각 CASE는 테이블 또는 URI 중 하나에 매핑됩니다. 예 : TABLE1 또는 URI1 앱이 사용하는 모든 테이블에 대해 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]

3

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

참조 :

참고 : 향후 데모 나 기사의 링크가 제거 될 수 있기 때문에 코드를 복사했습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.