8、更新操作详解:update() 方法实现、影响行数、条件更新
更新操作,说白了就是改数据。在 ContentProvider 里,update() 是四大操作里最容易踩坑的一个。我见过太多人把 delete 和 update 搞混,或者更新完发现影响行数不对,一脸懵。
今天咱们就把 update() 彻底聊透。从方法签名到实现细节,从条件更新到事务安全,一个都不放过。
8.1 update() 方法签名
先看接口长什么样:
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
// 你的实现
}
四个参数,一个返回值。我来拆开讲:
- uri:操作路径,决定了你要更新哪张表
- values:要改的新数据,键值对形式
- selection:条件语句,类似 SQL 的 WHERE
- selectionArgs:条件参数,防 SQL 注入用的
- 返回值:影响的行数,int 类型
返回值很多人忽略。其实它很重要——客户端可以通过它判断更新是否成功。
核心要点:update() 返回的是实际被修改的行数,不是受影响的行数。如果某行数据没变化,它不会被计入。
8.2 基础实现:一个完整的 update()
直接上代码。这是我个人习惯的写法:
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count;
String tableName;
switch (uriMatcher.match(uri)) {
case CODE_BOOK:
tableName = TABLE_BOOKS;
break;
case CODE_BOOK_ID:
tableName = TABLE_BOOKS;
// 追加 ID 条件
String id = uri.getLastPathSegment();
if (selection != null && !selection.isEmpty()) {
selection = selection + " AND " + COLUMN_ID + "=?";
} else {
selection = COLUMN_ID + "=?";
}
// 合并参数
String[] newArgs;
if (selectionArgs != null && selectionArgs.length > 0) {
newArgs = new String[selectionArgs.length + 1];
System.arraycopy(selectionArgs, 0, newArgs, 0, selectionArgs.length);
newArgs[selectionArgs.length] = id;
} else {
newArgs = new String[]{id};
}
selectionArgs = newArgs;
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
count = db.update(tableName, values, selection, selectionArgs);
// 通知观察者
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
嗯,这里要注意:更新完一定要通知观察者。不然 UI 那边不会刷新,用户会以为操作没生效。
8.3 影响行数的秘密
为什么 update() 返回的行数有时候和预期不一样?我遇到过好几次这种问题。
举个例子:
ContentValues values = new ContentValues();
values.put("title", "Android 进阶");
values.put("price", 59.9);
int rows = getContentResolver().update(
bookUri,
values,
"title = ?",
new String[]{"Android 进阶"}
);
你猜 rows 是多少?
如果表里已经有一行 title 是 "Android 进阶",price 已经是 59.9,那 rows 就是 0。因为 SQLite 很聪明——数据没变,它就不算更新。
避坑指南:我曾经在批量更新时,用影响行数判断是否全部成功。结果因为部分数据没变化,行数对不上,导致业务逻辑出错。后来我改成用事务+计数的方式来解决。
8.4 条件更新的几种写法
条件更新是 update() 的精髓。我总结了几种常见场景:
8.4.1 精确匹配
// 更新指定 ID 的书
selection = "_id = ?";
selectionArgs = new String[]{"42"};
8.4.2 范围匹配
// 更新价格大于 50 的书
selection = "price > ?";
selectionArgs = new String[]{"50"};
8.4.3 多条件组合
// 更新某个作者的书,且价格低于 30
selection = "author = ? AND price < ?";
selectionArgs = new String[]{"张三", "30"};
8.4.4 模糊匹配
// 更新标题包含 "Android" 的书
selection = "title LIKE ?";
selectionArgs = new String[]{"%Android%"};
警告:永远不要拼接 SQL 字符串!用 selectionArgs 传参。我见过有人这么写:
// 危险!不要这样做!
selection = "title = '" + userInput + "'";
这等于把数据库大门敞开。用户输入 "'; DROP TABLE books;--" 你就哭了。
8.5 批量更新与事务
如果你要更新多行数据,建议用事务包裹。为什么?
你想想看,如果更新 100 行,第 50 行失败了,前面 49 行已经改了。数据就不一致了。
我的做法是这样:
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int count = 0;
db.beginTransaction();
try {
count = db.update(TABLE_BOOKS, values, selection, selectionArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
这样要么全部更新成功,要么全部回滚。数据一致性有保障。
8.6 更新操作的性能考量
更新操作看似简单,但性能问题不容忽视。我列几个要点:
| 场景 | 建议 | 原因 |
|---|---|---|
| 更新单行 | 用主键条件 | 主键有索引,速度最快 |
| 更新多行 | 用索引列做条件 | 避免全表扫描 |
| 频繁更新 | 批量提交,不要逐条 | 减少事务开销 |
| 更新大字段 | 考虑异步执行 | 避免阻塞 UI 线程 |
个人经验:我做过一个笔记 App,用户打字时实时保存。一开始每条笔记更新都直接调 update(),结果卡顿明显。后来改成 500ms 防抖+批量提交,流畅多了。
8.7 update() 的完整流程图
下面这张图把 update() 的完整流程画出来了。从客户端调用到数据库写入,每一步都清楚:
8.8 常见问题与解决方案
最后,我把自己踩过的坑整理一下:
- 更新后 UI 不刷新:忘了调 notifyChange()。记得在 count > 0 时通知。
- 影响行数为 0:数据没变化,或者条件写错了。先查一下数据是否存在。
- 更新了不该更新的行:条件太宽泛。比如忘了加 ID 条件,结果全表都改了。
- 并发更新数据错乱:没有用事务。多线程同时更新同一行,后写的覆盖先写的。
- SQL 注入风险:拼接字符串。用 selectionArgs 传参就安全了。
我的习惯:每次写 update(),我都会先写一个测试用例,用已知数据验证影响行数。确认无误后再上线。这招帮我避免了好几次生产事故。
update() 看似简单,但细节决定成败。条件写对了,事务用好了,通知到位了,你的 ContentProvider 才算真正靠谱。
公众号:蓝海资料掘金营,微信deep3321