Buffalo框架默认不支持软删除,需手动实现:添加deleted_at字段、用BeforeDestroy钩子拦截硬删并更新时间戳、所有查询显式过滤deleted_at IS NULL、提供恢复和彻底删除方法。

Buffalo 框架默认不支持软删除
Buffalo 本身没有内置软删除机制,它依赖底层 ORM(通常是 Pop)做数据操作,而 Pop 的 Destroy 方法是硬删。如果你直接调用 user.Destroy(tx),记录会从数据库物理移除,无法恢复。
软删除必须手动实现:添加标记字段(如 deleted_at),改写查询逻辑,覆盖删除行为。Pop 支持通过钩子(BeforeDestroy)拦截删除动作,这是最常用且可靠的切入点。
在模型中定义 deleted_at 字段并注册钩子
以 User 模型为例,在结构体中加入 deleted_at 字段,并确保数据库迁移已创建该列:
type User struct {
ID uuid.UUID `json:"id" db:"id"`
Name string `json:"name" db:"name"`
DeletedAt time.Time `json:"deleted_at" db:"deleted_at"`
}
然后在模型的 BeforeDestroy 钩子中阻止硬删,改为更新时间戳:
- 钩子函数必须接收
*pop.Connection和指针接收者(如*User) - 返回
errors.New("soft delete")或其他非 nil error 可中断默认Destroy流程 - 显式调用
tx.Update(u)完成软删,注意只更新deleted_at,避免覆盖其他字段
示例钩子实现:
func (u *User) BeforeDestroy(tx *pop.Connection) error {
u.DeletedAt = time.Now()
return tx.Update(u, "deleted_at")
}
所有查询必须默认过滤已软删除的记录
Pop 不会自动忽略 deleted_at IS NOT NULL 的行,你必须在每次查询时显式排除。最稳妥的方式是封装一个带过滤的查找方法:
- 不要依赖全局 scope(Pop 不支持 ActiveRecord 风格的默认 scope)
- 避免在控制器里反复写
where deleted_at is null,容易遗漏 - 推荐为每个模型添加类似
FindActiveUser的方法,内部使用Where("deleted_at is null")
例如:
func FindActiveUser(tx *pop.Connection, id uuid.UUID) (*User, error) {
u := &User{}
err := tx.Where("id = ? AND deleted_at is null", id).First(u)
return u, err
}
如果项目中大量使用 Find,可考虑统一包装 tx.Where(...).All() 查询,但需注意关联查询(Eager)仍需单独处理软删条件。
恢复软删除记录与彻底清除需额外方法
软删除不是“暂停”,而是状态切换,因此需要明确的恢复和清空路径:
- 恢复:更新
deleted_at为NULL,Pop 支持传入nil值,但需用db:"deleted_at,null"标签或显式 SQL - 彻底删除:绕过钩子,用
tx.Raw(...).Exec()执行原生DELETE FROM users WHERE ... - 批量操作(如清空回收站)务必加事务,并确认是否影响关联数据(如外键级联)
恢复示例:
func (u *User) Restore(tx *pop.Connection) error {
u.DeletedAt = time.Time{} // 注意:Pop 会将零值转为 NULL,前提是字段声明含 `,null`
return tx.Update(u, "deleted_at")
}
字段标签建议写成:DeletedAt time.Time `json:"deleted_at" db:"deleted_at,null"`,否则零值可能被忽略。
软删除真正难的不是加字段或写钩子,而是让整个代码库的读写逻辑对 deleted_at 保持一致——漏掉一次查询过滤,就可能把“已删除”数据暴露给前端或参与业务计算。

















