4. CRUD 操作基础:四大核心方法的实现与参数详解
好,咱们进入 ContentProvider 最核心的部分了。
说白了,ContentProvider 就是个数据中间人。你想想看,A 进程想访问 B 进程的数据,总不能直接连数据库吧?这时候就需要 ContentProvider 站出来,提供一套标准接口。这套接口就是 CRUD——增、删、改、查。
我个人习惯把这四个方法叫做「四大金刚」。今天咱们就把它们拆开揉碎了讲清楚。
4.1 query():查询数据
query() 是使用频率最高的方法。我刚开始做 ContentProvider 时,以为它和 SQLiteDatabase 的 query() 差不多。后来发现,坑还挺多。
先看方法签名:
public abstract Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
参数一个一个说:
- uri:操作的数据路径。比如
content://com.example.provider/user/1表示 id 为 1 的用户。 - projection:要查询的列名。传 null 表示查所有列。但我建议别传 null,显式指定列名能提升性能。
- selection:查询条件。比如
"name = ?"。 - selectionArgs:条件参数。对应 selection 里的问号。
- sortOrder:排序方式。比如
"age DESC"。
重点来了:query() 返回的是 Cursor。Cursor 是个游标,指向结果集。用完必须关闭,否则会内存泄漏。我见过太多新手忘记关 Cursor,导致应用越用越卡。
实际项目中,我一般这样写:
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
int match = uriMatcher.match(uri);
switch (match) {
case USER_DIR:
return db.query(TABLE_USER, projection, selection,
selectionArgs, null, null, sortOrder);
case USER_ITEM:
String where = "_id = ?";
String[] whereArgs = new String[]{uri.getLastPathSegment()};
return db.query(TABLE_USER, projection, where,
whereArgs, null, null, sortOrder);
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
我的经验:query() 里尽量用 getReadableDatabase(),别用 getWritableDatabase()。读操作没必要拿写锁,能提升并发性能。
4.2 insert():插入数据
insert() 负责把数据写进去。方法签名:
public abstract Uri insert(Uri uri, ContentValues values)
参数就两个:
- uri:目标路径。注意,插入操作通常用目录型 URI,比如
content://provider/user。 - values:要插入的数据,用 ContentValues 封装。它就是个键值对容器,key 是列名,value 是值。
返回值是 Uri,指向新插入的行。这个设计很巧妙——客户端拿到 Uri 后,可以直接用它去查询刚插入的数据。
我曾经踩过一个坑:ContentValues 里没传主键值,但表的主键是自增的。结果 insert() 返回的 Uri 里包含的主键值,和实际插入的不一致。后来发现是 ContentProvider 实现里忘了调用 ContentUris.withAppendedId()。
正确的写法:
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int match = uriMatcher.match(uri);
if (match != USER_DIR) {
throw new IllegalArgumentException("Insert not supported for URI: " + uri);
}
long id = db.insert(TABLE_USER, null, values);
if (id > 0) {
Uri resultUri = ContentUris.withAppendedId(uri, id);
getContext().getContentResolver().notifyChange(resultUri, null);
return resultUri;
}
throw new SQLException("Failed to insert row into " + uri);
}
注意:insert() 成功后一定要调用 notifyChange()。否则客户端那边收不到数据变化通知,界面不会刷新。这个坑我至少帮三个同事排查过。
4.3 update():更新数据
update() 用来修改已有数据。方法签名:
public abstract int update(Uri uri, ContentValues values,
String selection, String[] selectionArgs)
参数和 query() 很像,多了一个 values:
- values:要更新的字段和值。只传需要修改的列就行。
- selection/selectionArgs:定位要更新的行。
返回值是受影响的行数。这个数字很有用——如果返回 0,说明没有匹配到任何行。
我个人习惯在 update() 里做两件事:
- 校验 values 不能为空
- 更新后通知数据变化
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int match = uriMatcher.match(uri);
int count;
switch (match) {
case USER_ITEM:
String id = uri.getLastPathSegment();
String where = "_id = ?";
String[] whereArgs = new String[]{id};
count = db.update(TABLE_USER, values, where, whereArgs);
break;
case USER_DIR:
count = db.update(TABLE_USER, values, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
避坑指南:我曾经在 update() 里忘了拼接 WHERE 条件,结果一次更新了整张表。嗯,从那以后我每次写 update() 都会先检查 selection 参数是否合理。对于单行更新,我强烈建议用 USER_ITEM 这种带 ID 的 URI,能避免误操作。
4.4 delete():删除数据
delete() 是四大金刚里最「危险」的一个。方法签名:
public abstract int delete(Uri uri, String selection, String[] selectionArgs)
参数和 update() 类似,但没有 values。返回值是删除的行数。
为什么说它危险?因为一旦 selection 传 null,整张表的数据就没了。我见过生产环境出过这种事故——客户端传了个空 selection,服务端直接清空了用户表。
所以我的实现里,会加一层保护:
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int match = uriMatcher.match(uri);
int count;
switch (match) {
case USER_ITEM:
String id = uri.getLastPathSegment();
count = db.delete(TABLE_USER, "_id = ?", new String[]{id});
break;
case USER_DIR:
// 防止误删全表
if (selection == null || selection.isEmpty()) {
throw new IllegalArgumentException(
"Delete all rows is not allowed. Use a specific URI.");
}
count = db.delete(TABLE_USER, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
重要提醒:delete() 和 update() 一样,成功后要 notifyChange()。另外,我建议在 ContentProvider 里统一做权限校验——不是所有客户端都能随便删数据的。
4.5 四大方法的共性要点
总结一下,这四个方法有几个共同点:
| 要点 | 说明 |
|---|---|
| URI 匹配 | 每个方法都要先做 UriMatcher 匹配,确定操作的是哪张表、哪行数据 |
| 线程安全 | ContentProvider 的方法可能被多个线程同时调用。SQLiteDatabase 本身是线程安全的,但如果你用了其他资源,记得加锁 |
| 数据通知 | insert/update/delete 成功后,必须调用 notifyChange()。query() 不需要 |
| 异常处理 | 遇到非法 URI 或操作失败,抛出 IllegalArgumentException 或 SQLException |
最后说一句:这四个方法写起来不难,但细节决定成败。我见过太多 ContentProvider 实现,要么忘了通知,要么 URI 匹配写错,要么 Cursor 没关。你想想看,跨进程数据源一旦出问题,排查起来比单进程麻烦得多。所以,写的时候多留个心眼,总没错。