`
yanmin6767
  • 浏览: 32410 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

[Android 数据库]一种典型的Content Provider 代码架构

阅读更多

我们平时在做android开发的时候,一定经常会接触到数据库操作,android使用sqlite作为它的本地数据库,并提供了一种叫做Content Provider的数据访问机制,简单来说,它就像一个web服务,有自己的URI,我们也是通过URI的形式来访问它的数据,通过这种形式的接口,使得我们的数据不仅在我们自己的应用中可以访问,甚至还可以被系统中的其他应用所调用。 一个典型的例子就是我们手机中的通讯录,android给我们暴露了一个接口,我们只要申请到相应的权限,通过访问这个接口,就可以得到通讯录的信息了。 说了这么多,现在言归正传,这篇文章主要是和大家分享一下Content Provider的实现方式,通过一些更加标准的代码架构,可以使我们的项目的效率更高,并且提高可维护性。

     例如,我们有一个记事本程序,需要记录每一条记事,那么我们就需要这样一个数据表:

note

_id                 integer
content         varchar(2000)
pubDate         integer



    为了说明问题,我们只位这个表设置了三个字段,分别使记录的id,记事内容,和编辑时间, 大家需要注意个是id字段需要在前面加一个下划线,否则在和ListView绑定的时候会出问题。

    现在有了表接口,那么我们就可以将这张表抽象程一个数据结构,代码如下:

  1. public class NoteMetaData {
  2.    
  3.     public static final String AUTHORITY = "org.spring.provider.NoteProvider";
  4.    
  5.     public static final String DATABASE_NAME = "note.db";
  6.    
  7.     public static final int DATABASE_VERSION = 1;
  8.    
  9.    
  10.     private NoteMetaData() {}
  11.    
  12.     public static final class NoteTableMetaData implements BaseColumns{
  13.         
  14.         private NoteTableMetaData() {};
  15.         
  16.         public static final String TABLE_NAME = "note";
  17.         
  18.         public static final String DEFAULT_SORT_ORDER  = "_id desc";
  19.         
  20.         public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/notes");
  21.         
  22.         public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.spring.demo";
  23.         
  24.         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.spring.demo";
  25.         
  26.         //integer
  27.         public static final String NOTE_ID = "_id";
  28.         
  29.         //string
  30.         public static final String NOTE_CONTENT = "content";
  31.         
  32.         //integer
  33.         public static final String NOTE_PUB_DATE = "pubDate";
  34.         
  35.     }
  36.    
  37. }
复制代码

如上代码,我们定义好了我们需要的元数据,这个类首先定义了和ContentProvider相关的信息,AUTHORITY表示ContentProvider的URI,DATABASE_NAME和DATABASE_VERSION分别表示我们的数据库名和数据库版本。静态内部类NoteTableMetaData,表示我们刚才定义个表,首先TABLE_NAME表示表名,DEFAULT_SORT_ORDER 作为默认的排序规则,当然如果我们在查询中自定义了排序规则的话,这个就会被覆盖,CONTENT_URI定义了Provider对外访问的协议,CONTENT_TYPE,CONTENT_ITEM_TYPE用于表示两种uri模式的实体类型,这个其实很像我们http协议中的MIME类型。接下来的数据就是我们数据库字段的映射了,可以用注释标出他们的数据类型,这样,我们的元数据就完工拉,这个工作虽然有些枯燥,但是对于我们项目的可维护性还是很有帮助的,例如如果我们需要改动某个字段的名字,我们只需要修改元数据类就完成了所有的工作。

    有了这些源数据,我们就可以开始来写ContentProvider了,首先,我们要定义一个默认的映射表,代码如下:

  1. public class NoteProvider extends ContentProvider{

  2.     private static HashMap<String, String> noteProjectionMap;
  3.    
  4.     static {
  5.         
  6.         noteProjectionMap = new HashMap<String, String>();
  7.         
  8.         noteProjectionMap.put(NoteTableMetaData.NOTE_ID, NoteTableMetaData.NOTE_ID);
  9.         noteProjectionMap.put(NoteTableMetaData.NOTE_CONTENT, NoteTableMetaData.NOTE_CONTENT);
  10.         noteProjectionMap.put(NoteTableMetaData.NOTE_PUB_DATE, NoteTableMetaData.NOTE_PUB_DATE);
  11.         
  12.     }
复制代码

这个HashMap其实就相当于我们select语句中的别名,在后面的sqlite操作中会用到这个数据,一般情况下,我们不需要更改别名,所以在map中将键和值都设置为同样的就可以了。
     由于我们的NoteProvider要处理两种形式的URI,所以我们需要一个机制来区分不同的URI,这就要用到UriMatcher,代码如下:

  1.     private static UriMatcher matcher;
  2.    
  3.     private static final int QUERY_LIST = 1;
  4.     private static final int QUERY_ITEM = 2;
  5.    
  6.     static {
  7.         
  8.         matcher = new UriMatcher(UriMatcher.NO_MATCH);
  9.         matcher.addURI(NoteMetaData.AUTHORITY, "notes", QUERY_LIST);
  10.         matcher.addURI(NoteMetaData.AUTHORITY, "notes/#", QUERY_ITEM);
  11.                
  12.     }   
复制代码

UriMatcher用来区分不同的URI,首先我们将它定义为一个静态属性,然后在静态初始化块中,使用它的addURI方法,为它添加了两个URI规则,并为每个规则设置了一个表示常量,这里有QUERY_LIST和QUERY_ITEM,而这个方法的前两个参数就是用来构造这个URI规则的,例如第一条规则中,第一个参数我们用到了元数据中的 AUTHORITY和一个notes字符串, 这样,这条规则最终就会成为这种形式org.spring.provider.NoteProvider/notes 而另外一条规则就是这样org.spring.provider.NoteProvider/notes/# ,注意到第二条规则中的井号,它是一个占位符,在实际的场景中,这个位置会用一个数字来代替。说了这么多,我们为什么要用两个URI来为这个Provider来提供接口呢,相信只要做过web开发的朋友就会知道,假如我们要做一个CRUD功能,我们首先需要一个页面来显示数据,这个页面是不接受ID参数的。但我们还需要有一个编辑功能的页面,而这个页面就需要接受一个ID来区分要编辑哪一条记录。这样就不难理解我们为什么要用两种URI了,这里的第一条URI规则,就相当于那个显示数据的页面,而第二个URI 中的井号的位置就相当于编辑页面中的ID。有了这个matcher后,我们就可以根据不同的URI开执行各自所需的操作。

    现在Provider的基本信息已经基本完成了,因为我们的Provider需要和数据库进行交互,所以我们还需要一个中间层,可以使用SQLiteOpenHelper。

  1.     private DatabaseHelper dbHelper;
  2.    
  3.     class DatabaseHelper extends SQLiteOpenHelper{

  4.         DatabaseHelper(Context context) {
  5.             
  6.             super(context,   NoteMetaData.DATABASE_NAME, null, NoteMetaData.DATABASE_VERSION);   
  7.         }
  8.         
  9.         @Override
  10.         public void onCreate(SQLiteDatabase db) {
  11.             
  12.             db.execSQL(
  13.                     
  14.                     "create table " + NoteTableMetaData.TABLE_NAME + " ( "
  15.                     + NoteTableMetaData.NOTE_ID + " integer primary key, "
  16.                     + NoteTableMetaData.NOTE_CONTENT + " varchar(2000), "
  17.                     + NoteTableMetaData.NOTE_PUB_DATE + " integer"
  18.                     + ");"
  19.                     
  20.             );
  21.         }

  22.         @Override
  23.         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
  24.             
  25.             db.execSQL("drop table if exists " + NoteTableMetaData.TABLE_NAME);
  26.             onCreate(db);
  27.             
  28.         }
  29.         
  30.         
  31.     }
复制代码

如上代码,我们扩展了自己的Helper,并将它定义为Provider 的私有属性,这个类的实现中我们还是使用元数据来进行操作,例如创建数据库和更新数据库的操作,字段名和表名使用的是元数据中的属性。

     现在有了这些基础架构后,我们就可以实现相应的数据库操作方法了,先从query说起:

  1.     public Cursor query(Uri uri, String[] projection, String selection,
  2.             String[] selectionArgs, String sortOrder) {
  3.         
  4.         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
  5.         
  6.         switch (matcher.match(uri)) {
  7.         
  8.             case QUERY_LIST:{
  9.                
  10.                 qb.setTables(NoteTableMetaData.TABLE_NAME);
  11.                 qb.setProjectionMap(noteProjectionMap);
  12.                
  13.             }break;
  14.             
  15.             case QUERY_ITEM:{
  16.                
  17.                 qb.setTables(NoteTableMetaData.TABLE_NAME);
  18.                 qb.setProjectionMap(noteProjectionMap);
  19.                 qb.appendWhere(NoteTableMetaData.NOTE_ID + "=" + uri.getPathSegments().get(1));
  20.                
  21.             }break;
  22.             
  23.         default:
  24.             throw new IllegalArgumentException("Unknown URI " + uri);
  25.         }
  26.         
  27.         String orderBy;
  28.         if(TextUtils.isEmpty(sortOrder)) {
  29.             
  30.             orderBy = NoteTableMetaData.DEFAULT_SORT_ORDER;
  31.             
  32.         }else {
  33.             
  34.             orderBy = sortOrder;
  35.         }
  36.         
  37.         SQLiteDatabase db = dbHelper.getWritableDatabase();
  38.         
  39.         Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
  40.         
  41.         c.setNotificationUri(getContext().getContentResolver(), uri);
  42.         return c;
  43.         
  44.     }
复制代码

这里使用了SQLiteQueryBuilder来构建数据库查询,这样可使我们从更抽象的层次来处理数据库交互,在这里我们之前定义的matcher就派上用场拉,我们判断了一下URI的类型,如果使QUERY_LIST,就查询所有的数据,而如果是QUERY_ITEM类型,就只查询特定ID下的记录。这一点在我们的代码中应该很明显,当然如果给出的URI不符合任意一条规则,那么就直接抛出异常。 接下来我判断了一下这次查询是否明确指定了排序规则,如果没指定就使用我们之前定一个默认规则来对数据进行排序,随后就是获取数据库引用,并执行插叙操作了。 这里要注意一下c.setNotificationUri方法,这个方法为当的URI注册了一下通知,简单来说就是这样,如果有其他的调用改变了底层数据库并发送了更改消息,那么这些注册了通知的URI会自动更新他们的数据集,而不用我们手动的进行刷新,这样可以省去很多繁琐的编码工作。最典型的例子就是为ListView绑定数据时,如果使用了这种机制,我们在修改数据库中的数据后,ListView 的显示也会自动的刷新。

      接下来介绍一下insert方法:

  1.     public Uri insert(Uri uri, ContentValues values) {
  2.         
  3.         if(matcher.match(uri) != QUERY_LIST) {
  4.             
  5.             throw new IllegalArgumentException("Unknown URI " + uri);
  6.             
  7.         }
  8.         
  9.         
  10.         
  11.         if(values.containsKey(NoteTableMetaData.NOTE_PUB_DATE) == false) {
  12.             
  13.             Long now = Long.valueOf(System.currentTimeMillis());
  14.             values.put(NoteTableMetaData.NOTE_PUB_DATE, now);
  15.             
  16.         }
  17.         
  18.         SQLiteDatabase db = dbHelper.getWritableDatabase();
  19.         
  20.         long rowID = db.insert(NoteTableMetaData.TABLE_NAME, NoteTableMetaData.NOTE_CONTENT, values);
  21.         
  22.         if(rowID > 0) {
  23.             
  24.             Uri insertedUri = ContentUris.withAppendedId(NoteTableMetaData.CONTENT_URI, rowID);
  25.             getContext().getContentResolver().notifyChange(insertedUri, null);
  26.             return insertedUri;
  27.             
  28.         }
  29.         
  30.         throw new android.database.SQLException("Failed to insert row into " + uri);
  31.     }
复制代码

先说一下这个方法传进来的参数,第一个参数时调用的URI,接下来的values就相当于insert语句中的列名和值的一对组合,由于这个插入方法只接受不带ID的URI所以我们在一开始的时候进行了一下判断,然后我们检测了一下是否指定了日期,如果没有就以当前时间作为默认值,随后就是数据库调用了。  我们通过返回rowID来判断该条记录是否插入成功,如果成功就返回带着这条记录ID的URI,否则就抛出异常。注意到这里的notifyChange方法,这个正好和前面的注册通知相对应,它会通知所有注册的URI,数据库已经改变。

    下面再来介绍一下update方法:

  1.     @Override
  2.     public int update(Uri uri, ContentValues values, String selection,
  3.             String[] selectionArgs) {
  4.         
  5.         SQLiteDatabase db = dbHelper.getWritableDatabase();
  6.         int count;
  7.         
  8.         switch (matcher.match(uri)) {
  9.         
  10.             case QUERY_LIST:{
  11.                
  12.                 count = db.update(NoteTableMetaData.TABLE_NAME, values,selection, selectionArgs);
  13.                
  14.             }break;
  15.             
  16.             case QUERY_ITEM:{
  17.                
  18.                String rowID = uri.getPathSegments().get(1);
  19.                count = db.update(NoteTableMetaData.TABLE_NAME, values,
  20.                             NoteTableMetaData.NOTE_ID + "=" + rowID
  21.                             + (!TextUtils.isEmpty(selection) ? ("and ( " + selection + "  ) ") : ""), selectionArgs);
  22.                
  23.             }break;

  24.         default:
  25.             
  26.             throw new IllegalArgumentException("Unknown URI " + uri);
  27.         }
  28.         
  29.         getContext().getContentResolver().notifyChange(uri, null);
  30.         return count;
  31.     }
复制代码

这里个方法里的内容和前面很类似,唯一的区别就是通过URI来确定更新的方式。这一点在代码中写的也比较明白,所以就不赘述了。最后再介绍一下delete 方法:

  1.     @Override
  2.     public int delete(Uri uri, String selection, String[] selectionArgs) {
  3.         
  4.         SQLiteDatabase db = dbHelper.getWritableDatabase();
  5.         int count;
  6.         
  7.         switch (matcher.match(uri)) {
  8.         
  9.             case QUERY_LIST:{
  10.                
  11.                 count = db.delete(NoteTableMetaData.TABLE_NAME, selection, selectionArgs);
  12.                
  13.             }break;
  14.             
  15.             case QUERY_ITEM:{
  16.                
  17.                 String rowID = uri.getPathSegments().get(1);
  18.                 count = db.delete(NoteTableMetaData.TABLE_NAME,
  19.                         NoteTableMetaData.NOTE_ID + "="  + rowID +
  20.                         (!TextUtils.isEmpty(selection) ? (" and ( " + selection + " ) ") : "" ), selectionArgs);
  21.                
  22.             }break;
  23.         default:
  24.             throw new IllegalArgumentException("Unknown URI " + uri);
  25.         }
  26.         
  27.         getContext().getContentResolver().notifyChange(uri, null);
  28.         return count;
  29.      
  30.     }
复制代码

这个方法也是判断两种不同的URI,如果时QUERY_LIST类型的URI,那么它就会删除所有的记录(当然,如果我们不需要这种操作,也可以忽略这种URI),另一种就是根据ID来删除相应的记录。这个方法应该也不难理解。 大家注意到,我们在这些方法中,大量的用到了我们元数据类中的信息,这样做的好处前面也说过了,隔离了底层的表结构后,让数据库结构的修改变得非常容易。

     当然,我们还需要实现getType方法,来返回不同URI对应的MIME类型,这个信息,在我们的元数据中已有定义:

  1.     @Override
  2.     public String getType(Uri uri) {
  3.         
  4.         switch (matcher.match(uri)) {
  5.         
  6.             case QUERY_LIST:{
  7.                 return NoteTableMetaData.CONTENT_TYPE;
  8.             }
  9.             
  10.             case QUERY_ITEM:{
  11.                 return NoteTableMetaData.CONTENT_ITEM_TYPE;
  12.             }
  13.             default:{               
  14.                 throw new IllegalArgumentException("Unknown URI " + uri);
  15.             }
  16.         }
  17.         
  18.     }
复制代码

好啦,到现在位置我们的Provider就已经实现好了,最后不要忘了在manifest中注册这个Provider:

  1.         <provider android:name=".NoteProvider" android:authorities="org.spring.provider.NoteProvider"  />
复制代码

到此为止,我们的数据访问接口就实现完成了,我们可以用下面的方式很容易的进行数据访问:

  1.         //插入数据
  2.         ContentValues values = new ContentValues();
  3.         values.put(NoteTableMetaData.NOTE_CONTENT, "test");         
  4.         Uri insertedUri = getContentResolver().insert(NoteTableMetaData.CONTENT_URI, values);   
  5.         
  6.         //更新数据
  7.         values.put(NoteTableMetaData.NOTE_CONTENT, "test updated");     
  8.         getContentResolver().update(Uri.withAppendedPath(NoteTableMetaData.CONTENT_URI, "/" + insertedUri.getPathSegments().get(1)), values, null, null);
  9.         
  10.         //查询数据
  11.         Cursor c = getContentResolver().query(NoteTableMetaData.CONTENT_URI, null, null, null, null);
  12.             
  13.         //绑定ListView
  14.         SimpleCursorAdapter adapter =
  15.             new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, c,
  16.                     new String[] {"content" }, new int[] {android.R.id.text1});
  17.         
  18.         list.setAdapter(adapter);
  19.         
  20.         //删除数据
  21.         getContentResolver().delete(insertedUri, null, null);
复制代码

这篇文章主要是给大家提供了一种ContentProvider的架构方法,通过一些良好的代码组织,可以让我们的开发工作变得更加轻松,并且有效的增强应用的建壮性,对我们日常的开发还是很有帮助的。当然这种方法并不是唯一的,更不敢说这个是最好的。更希望它能够起到一种抛砖引玉的作用,大家可以在这个基础之上进一步的探索,找出更加适合自己的架构方式~

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics