在 Android 中使用内容提供程序公开多个表的最佳实践

我在开发一个应用程序,里面有活动表和场地表。我希望能够授予其他应用程序对此数据的访问权限。我有几个关于这类问题的最佳实践的问题。

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

  2. 如何为多个表设计内容提供程序? 扩展前面的问题,我应该为整个应用程序使用一个内容提供程序,还是应该为活动和场地创建单独的提供程序?

我发现大多数示例只处理单个表应用程序,所以我希望这里有任何提示。

43665 次浏览

I recommend checking out the source code for the Android 2.x ContactProvider. (Which can be found online). 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. You'll probably also want to provide a class or two for holding constants for your table field names and URI strings. These classes could be provided either as an API include or as a drop in class, and will make it much easier for the consuming application to use.

Its a bit complex, so you might also want to check out how the calendar as well to get an idea of what you do and do not need.

You should only need a single DB adapter and a single Content provider per database (not per table) to do most of the work, but you can use multiple adapters/providers if you really want to. It just makes things a bit more complicated.

It's probably a bit late for you, but others may find this useful.

First you need to create multiple 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");

Then you expand your 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);
}

Then create your tables

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

Don't forget to add the second DATABASE_CREATE to onCreate()

You are going to use a switch-case block to determine what table is used. This is my insert code

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

You will need to devide up the delete, update, getType, etc. Wherever your provider calls for DATABASE_TABLE or CONTENT_URI you will add a case and have DATABASE_TABLE1 or CONTENT_URI1 in one and #2 in the next and so on for as many as you want.

One ContentProvider can serve multiple tables, but they should be somewhat related. It will make a difference if you intend to sync your providers. If you want seperate syncs for, let's say Contacts, Mail or Calendar, you will need different providers for each of them, even if they end up being in the same database or are synced with the same service, because Sync Adapters are tied directly to a particular provider.

As far as I can tell, you can only use a single SQLiteOpenHelper per database, though, since it stores its meta information in a table within the database. So if your ContentProviders access the same database, you'll have to share the Helper somewhow.

Note: This is a clarification/modification to the answer provide by Opy.

This approach subdivides each of the insert, delete, update, and getType methods with switch statements in order to handle each of your individual tables. You'll use a CASE to identify each table (or uri) to be referenced. Each CASE then maps to one of your tables or URI's. E.g., TABLE1 or URI1 is selected in CASE #1, etc. for all the tables your app employs.

Here's an example of the approach. This is for the insert method. It's implemented a bit differently from Opy's but performs the same function. You can select the style you prefer. I also wanted to be sure insert returns a value even if the table insertion fails. In that case it returns 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]

I found best demo and explanation for ContentProvider and I think it has followed Android Standards.

Contract Classes

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

and Inner Classes:

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

Now creating Database using 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 + "));"
);
}
}

Content Provider:

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

I hope it will helps you.

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

Full Article : https://guides.codepath.com/android/creating-content-providers

References:

Note : I copied code just because if link of demo or article may be remove in future.