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 做删除了。

记住三条铁律:

  1. 永远用 db.delete(),别用 execSQL()
  2. 删除后必须 notifyChange()
  3. 级联删除一定要包在事务里

9.6 知识体系总览

下面这张图把删除操作的几个核心维度串起来了:

删除操作 delete() 实现 URI 匹配解析 SQL 执行 notifyChange() 级联删除 外键 CASCADE 手动级联 事务包裹 软删除策略 is_deleted 字段 查询时过滤 定时物理清理

删除操作看似简单,但选错策略,后面维护起来就是噩梦。我个人建议:能软删就别硬删,能自动级联就别手动写。少给自己挖坑,多给用户留条后路。


公众号:蓝海资料掘金营,微信deep3321