软删除字段应命名为 deleted 或 isDeleted,统一用布尔类型;查询需自动过滤 deleted: true 文档,推荐 ODM 层预设条件或封装工具函数;唯一性校验应结合 deleted 字段建复合索引或部分索引;彻底删除必须走应用层服务方法,确保关联清理、审计日志与事务一致性。

软删除字段该叫什么名字才不容易踩坑
直接用 deleted 或 isDeleted 是最稳妥的选择,别图省事写成 del、rm 或 status。前者语义清晰,团队协作和后续聚合查询都少歧义;后者在多人维护或对接 BI 工具时,极易被误判为业务状态字段。
常见错误现象:status: "deleted" 这种字符串枚举方式,会导致索引效率低、无法用布尔索引加速、且容易漏掉大小写或空格(比如 "Deleted")。
- 统一用布尔型
deleted: true/false,默认false - 配合 TTL 索引做物理清理时,也依赖这个字段做条件筛选,类型不一致会失效
- 如果已有历史数据用字符串表示,迁移时务必批量更新,不要留混合类型
MongoDB 查询时如何自动过滤已软删除文档
靠应用层手动加 { deleted: { $ne: true } } 最常见,但容易漏——尤其在聚合管道、$lookup、$facet 里,一不留神就查出“幽灵数据”。更可靠的方式是封装一个基础查询构造器,或者在 ODM 层(如 Mongoose)用 pre-hook 拦截。
使用场景:所有读操作(find、findOne、countDocuments、aggregate)都应默认排除 deleted: true 文档,除非显式传参要求包含。
- Mongoose 示例:定义 schema 时加
query: { deleted: { $ne: true } },它会自动注入到所有 find 类查询中 - 原生 driver 不支持自动注入,必须在每个
find()调用前拼接条件,推荐抽成工具函数nonDeletedQuery(baseQuery) -
updateOne和deleteOne默认不应修改deleted字段,除非操作明确是“恢复”或“彻底删除”
软删除后还能不能用 _id 做唯一性校验
能,但要注意:软删除文档仍占用 _id,所以新建同名文档(比如用户重注册)不会冲突,这是对的;但如果你依赖 _id + deleted 组合做业务唯一约束(例如“一个手机号只能有一个未删除用户”),MongoDB 默认索引不覆盖这个逻辑。
性能影响:加复合唯一索引 { phone: 1, deleted: 1 } 可解决,但要注意 deleted: true 的文档量大时,索引体积和写入开销会上升。
- 正确做法:建唯一索引
{ phone: 1, deleted: 1 },并确保插入时显式指定deleted: false - 错误做法:只建
{ phone: 1 },结果允许插入多个deleted: true的同手机号文档,后续恢复时可能冲突 - 注意:MongoDB 4.2+ 支持部分索引,可建
{ phone: 1 }+partialFilterExpression: { deleted: false },更省空间
彻底删除软删除文档时为什么不能只删 _id
因为软删除往往伴随关联清理(比如用户软删后要清空其 session、通知、草稿),只执行 deleteOne({ _id: xxx }) 会跳过这些逻辑,留下脏数据。而且,如果业务上需要审计“谁在什么时候彻底删了谁”,单靠 deleteOne 无法记录操作上下文。
容易被忽略的地方:很多团队把“彻底删除”当成 DBA 手动操作,绕过应用层,结果权限、日志、事务一致性全丢了。
- 必须走应用层服务方法,比如
hardDeleteUser(id, operatorId),里面包含事务、日志、钩子调用 - 对应 MongoDB 操作应是
findOneAndDelete({ _id: xxx, deleted: true }),双重校验防止误删活跃文档 - 如果用了 Change Streams,记得监听
delete事件而非仅靠update事件捕获软删除


















