9、删除操作详解:delete() 方法实现、级联删除、软删除策略
删除操作,听起来简单,对吧?不就是把数据从数据库里抹掉嘛。但实际项目中,我在这上面栽过不少跟头。你想想看,一个用户删除了某条记录,关联的表怎么办?用户误操作了还能恢复吗?这些问题,今天咱们一个一个掰扯清楚。
9.1 delete() 方法的标准实现
ContentProvider 里的 delete() 方法,说白了就是接收 URI,解析参数,然后执行 SQL 删除。我见过很多新手直接写死 SQL,结果 URI 匹配一乱,删错了数据都不知道。
先看一个标准实现:
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int deletedCount;
SQLiteDatabase db = dbHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case CODE_USER:
// 删除单个用户
deletedCount = db.delete(
TABLE_USER,
selection,
selectionArgs
);
break;
case CODE_USER_ID:
// 根据 ID 删除
String userId = uri.getLastPathSegment();
deletedCount = db.delete(
TABLE_USER,
"_id = ?",
new String[]{userId}
);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
// 别忘了通知观察者
if (deletedCount > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return deletedCount;
}
核心要点:
- 一定要调用 notifyChange(),否则 UI 不会刷新
- selection 和 selectionArgs 要分开传,防止 SQL 注入
- 返回实际删除的行数,别瞎返回 0 或 -1
9.2 级联删除:别让数据变成孤儿
我在项目中遇到过这么个事:用户删了一篇博客,结果评论表里还挂着那条博客的 ID。下次查询评论时,关联的博客没了,直接崩了。这就是典型的「孤儿数据」问题。
级联删除有三种常见做法:
| 方案 | 实现方式 | 适用场景 |
|---|---|---|
| 数据库外键 + ON DELETE CASCADE | 建表时声明外键约束 | 强关联、数据一致性要求高 |
| 手动级联 | 在 delete() 里依次删除关联表 | 跨数据库、跨 ContentProvider |
| 触发器 | SQLite 触发器自动执行 | 复杂业务逻辑 |
方案一:数据库外键
SQLite 默认不开启外键,你得先执行 PRAGMA foreign_keys = ON;。建表时这样写:
CREATE TABLE comment (
_id INTEGER PRIMARY KEY,
content TEXT,
user_id INTEGER,
FOREIGN KEY (user_id) REFERENCES user(_id) ON DELETE CASCADE
);
这样删 user 时,关联的 comment 自动就没了。干净利落。
方案二:手动级联
有时候你没法改数据库结构,或者关联的数据在另一个 ContentProvider 里。这时候就得手动来:
case CODE_USER_ID:
String userId = uri.getLastPathSegment();
// 先删关联的评论
db.delete(TABLE_COMMENT, "user_id = ?", new String[]{userId});
// 再删用户
deletedCount = db.delete(TABLE_USER, "_id = ?", new String[]{userId});
break;
注意:手动级联一定要放在同一个事务里。否则删到一半崩了,评论没了用户还在,数据就乱套了。
9.3 软删除策略:给数据留条活路
硬删除就是直接 DELETE FROM。但很多业务场景下,用户删了东西,过两天又想恢复。你怎么办?数据已经没了。
软删除,说白了就是「假装删了」。给表加一个 is_deleted 字段,删除时只更新这个字段,不真删数据。
表结构设计:
CREATE TABLE user (
_id INTEGER PRIMARY KEY,
name TEXT,
email TEXT,
is_deleted INTEGER DEFAULT 0, -- 0: 正常, 1: 已删除
deleted_at TEXT -- 记录删除时间
);
删除操作变成更新:
ContentValues values = new ContentValues();
values.put("is_deleted", 1);
values.put("deleted_at", System.currentTimeMillis());
int count = db.update(
TABLE_USER,
values,
"_id = ?",
new String[]{userId}
);
查询时记得过滤:
Cursor cursor = db.query(
TABLE_USER,
null,
"is_deleted = 0", // 只查未删除的
null,
null,
null,
null
);
我的习惯:软删除的字段名统一叫 is_deleted,所有表都用同一个命名规范。这样写通用工具类时特别方便,不用每张表都去查字段名。
9.4 软删除 vs 硬删除,怎么选?
我一般这样判断:
- 用户数据、订单、文章 → 软删除。用户可能误操作,运营可能需要恢复数据。
- 日志、缓存、临时数据 → 硬删除。这些数据丢了也不心疼,留着还占空间。
- 敏感数据(密码、支付信息) → 硬删除。法律要求必须彻底清除。
还有一种混合策略:先软删除,过一段时间(比如30天)再物理清理。这样既给了用户后悔的机会,又不会让数据库无限膨胀。
// 定时清理 30 天前的软删除数据
String where = "is_deleted = 1 AND deleted_at < ?";
String[] args = new String[]{
String.valueOf(System.currentTimeMillis() - 30 * 24 * 60 * 60 * 1000L)
};
db.delete(TABLE_USER, where, args);
9.5 删除操作的避坑指南
我曾经犯过一个低级错误:在 delete() 方法里忘了调用 notifyChange()。结果数据删了,UI 上还显示着那条记录。用户点进去就崩溃,反馈炸了锅。
还有一次,我用了 db.execSQL("DELETE FROM user") 这种写法。结果 selection 参数完全没生效,整个表被清空了。嗯,从那以后我再也不敢用 execSQL 做删除了。
记住三条铁律:
- 永远用
db.delete(),别用execSQL() - 删除后必须
notifyChange() - 级联删除一定要包在事务里
9.6 知识体系总览
下面这张图把删除操作的几个核心维度串起来了:
删除操作看似简单,但选错策略,后面维护起来就是噩梦。我个人建议:能软删就别硬删,能自动级联就别手动写。少给自己挖坑,多给用户留条后路。
公众号:蓝海资料掘金营,微信deep3321