软删除必须用 deleted_at 而非 is_deleted:前者保留删除时间以支持恢复、归档与审计,后者无法区分删除时效;需通过自定义 SoftDeleteQuery 实现全局过滤,并封装 soft_delete 方法更新时间戳,禁用 session.delete();Flask-Admin 和 API 应隐藏 deleted_at,改用 is_deleted 计算字段,并为 (deleted_at, id) 添加联合索引。

软删除字段设计必须用 deleted_at 而不是布尔型 is_deleted
用布尔字段标记删除状态看似简单,但会丢失关键时间信息,导致无法做「7天内可恢复」或「按删除时间归档」等后续操作。更严重的是,is_deleted=True 无法区分「刚删」和「删了半年」,审计日志也无从追溯。
实际建表时,推荐统一使用 deleted_at(DateTime 类型,允许为 NULL):
created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) deleted_at = db.Column(db.DateTime, nullable=True)
这样所有未删除记录的 deleted_at 均为 None,查询时只需判断是否为 None 即可。
全局查询拦截必须重写 query_class,不能只靠手动加 .filter(deleted_at.is_(None))
手动在每个 Model.query.filter(...) 后补条件极易遗漏,尤其在关联查询、子查询或第三方扩展(如 Flask-Admin)中几乎不可控。
立即学习“Python免费学习笔记(深入)”;
正确做法是自定义一个带软删除逻辑的查询类,并绑定到基类模型:
class SoftDeleteQuery(BaseQuery):
def __new__(cls, *args, **kwargs):
obj = super().__new__(cls)
# 只对继承了 SoftDeleteMixin 的模型启用过滤
if hasattr(obj._mapper_zero(), 'deleted_at'):
obj = obj.filter_by(deleted_at=None)
return obj然后让所有模型继承该查询类:
class BaseModel(db.Model):
__abstract__ = True
query_class = SoftDeleteQuery
# ... 公共字段定义注意:如果某次真需要查已删除数据(比如后台回收站),可用 Model.with_deleted().filter(...),这需要额外在 SoftDeleteMixin 中实现 with_deleted() 类方法。
delete() 方法必须走事务 + 时间戳更新,禁止用 session.delete()
直接调用 session.delete(instance) 会触发物理删除,彻底丢失数据 —— 这正是要防止的误删根源。
应在模型中封装安全删除逻辑:
- 定义
soft_delete(self)实例方法,仅更新deleted_at - 确保该方法内部调用
db.session.commit()或由上层统一提交 - 若需级联软删除(如删除用户时软删其订单),必须显式遍历并调用各子对象的
soft_delete(),不能依赖cascade="all"(它会物理删除)
示例:
def soft_delete(self):
self.deleted_at = datetime.utcnow()
db.session.add(self) # 确保被追踪
# 不在此处 commit,交由业务层统一控制事务调用时:user.soft_delete(); db.session.commit()。
Flask-Admin 和 API 序列化必须显式处理 deleted_at 字段
Flask-Admin 默认展示所有字段,包括 deleted_at,暴露删除时间可能引发隐私或合规问题;API 返回 JSON 时若未过滤该字段,前端可能误判状态(比如把 "deleted_at": null 当作普通字段渲染)。
解决方式分两层:
- Flask-Admin:在
ModelView中设置column_exclude_list = ['deleted_at'],并重写get_query()和get_count_query()以跳过软删记录(除非专门做「回收站」视图) - 序列化(如用 Marshmallow):在 Schema 中排除
deleted_at,或添加load_only/dump_only控制方向;若需表达「是否已删」,应额外定义计算字段is_deleted,值为True当且仅当deleted_at is not None
别忽略数据库索引 —— 对高频查询的软删表,建议给 (deleted_at, id) 加联合索引,避免全表扫描。


















