vault backup: 2026-04-28 20:56:51
This commit is contained in:
+72
-18
@@ -11,18 +11,18 @@ create time: 2026-04-28 00:00
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[调用 db.Delete(&user)] --> SoftDel{"模型有<br/>DeletedAt 字段?"}
|
||||
A["调用 db.Delete(&user)"] --> SoftDel["模型有 DeletedAt 字段?"]
|
||||
|
||||
SoftDel --> |否| HardSQL["DELETE FROM users WHERE id = ?<br/>⚠️ 物理删除,不可恢复"]
|
||||
SoftDel --> |是| UpdateSQL["UPDATE users SET deleted_at = NOW() WHERE id = ?<br/>✅ 软删除,数据保留"]
|
||||
SoftDel --> |否|"HardSQL[DELETE FROM users WHERE id = ? ⚠️ 物理删除,不可恢复]"
|
||||
SoftDel --> |是|"UpdateSQL[UPDATE users SET deleted_at=NOW() WHERE id=? ✅ 保留数据]"
|
||||
|
||||
UpdateSQL --> Query{常规查询?}
|
||||
Query --> |是| Filtered["WHERE deleted_at IS NULL<br/>已删除记录自动隐藏"]
|
||||
Query --> |否| UnscopedQ{需要已删除数据?}
|
||||
UpdateSQL --> Query{"常规查询?"}
|
||||
Query --> |是| Filtered["WHERE deleted_at IS NULL\n已删除记录自动隐藏"]
|
||||
Query --> |否| UnscopedQ["需要已删除数据?"]
|
||||
UnscopedQ --> |是| IncludeAll["Unscoped() → 全部返回"]
|
||||
UnscopedQ --> |否| Filtered
|
||||
|
||||
style Start fill:#4FC08D,color:#fff
|
||||
style SoftDel fill:#4FC08D,color:#fff
|
||||
style Filtered fill:#3B82F6,color:#fff
|
||||
style HardSQL fill:#EF4444,color:#fff
|
||||
```
|
||||
@@ -39,7 +39,7 @@ type User struct {
|
||||
Name string `gorm:"size:64;not null"`
|
||||
}
|
||||
|
||||
// 等价的手动声明
|
||||
// 等价的手动声明(需 import "time")
|
||||
type Product struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:128;not null"`
|
||||
@@ -66,11 +66,16 @@ db.Delete(&user) // UPDATE users SET deleted_at=... WHERE id=?
|
||||
db.Unscoped().Delete(&user) // DELETE FROM users WHERE id=?
|
||||
// 彻底擦除,无法恢复!
|
||||
|
||||
// ===== 根据条件批量物理删除 =====
|
||||
// ===== 根据条件批量物理删除(配合清理策略使用)=====
|
||||
db.Unscoped().Where("deleted_at < ?", time.Now().AddDate(-90, 0, 0)).Delete(&User{})
|
||||
// 清理超过 90 天未激活的软删除记录
|
||||
// 彻底清除超过 90 天的软删除记录(注意用 Unscoped 否则查不到已删除行)
|
||||
```
|
||||
|
||||
> [!tip] 关键区别
|
||||
> - `db.Delete()` → **UPDATE** 语句,触发 BeforeUpdate / AfterUpdate 钩子
|
||||
> - `db.Unscoped().Delete()` → **DELETE** 语句,触发 BeforeDelete / AfterDelete 钩子
|
||||
> 两者走不同的钩子链,设计时需要考虑清楚业务逻辑该放在哪个钩子里。
|
||||
|
||||
## 查询已删除数据
|
||||
|
||||
### Unscoped — 忽略软删除过滤
|
||||
@@ -135,6 +140,23 @@ var user User
|
||||
db.Preload("Orders").First(&user, 1)
|
||||
// Preload 生成的 SQL 会自动加入 deleted_at IS NULL:
|
||||
// SELECT * FROM orders WHERE user_id = 1 AND deleted_at IS NULL;
|
||||
// 如果需要已删除的关联项,使用 Unscoped().Preload()
|
||||
```
|
||||
|
||||
### BelongsTo(反向)
|
||||
|
||||
BelongsTo 端同样受过滤影响——当通过子记录查询父记录时,如果父记录已被软删除,默认也不会被加载:
|
||||
|
||||
```go
|
||||
type Order struct {
|
||||
gorm.Model
|
||||
UserID uint
|
||||
User User `gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
var order Order
|
||||
db.Preload("User").First(&order, 1)
|
||||
// 如果该 Order 对应的 User 已被软删除,User 字段将为空
|
||||
```
|
||||
|
||||
### ManyToMany
|
||||
@@ -146,7 +168,7 @@ db.Preload("Orders").First(&user, 1)
|
||||
product := Product{Name: "Go Programming"}
|
||||
db.Model(&order).Association("Products").Append(&product)
|
||||
|
||||
// 如果要强制关联已删除的商品
|
||||
// 如果要强制关联已删除的商品(需 import "gorm.io/gorm/clause")
|
||||
db.Model(&order).Clauses(clause.OnConflict{DoNothing: true}).
|
||||
Association("Products").Append(&product)
|
||||
```
|
||||
@@ -194,6 +216,9 @@ type Article struct {
|
||||
// 在数据库层面创建 (slug, is_deleted) 联合唯一约束
|
||||
```
|
||||
|
||||
> [!tip] 最推荐的方案
|
||||
> 方案一(部分唯一索引)是 PostgreSQL 用户的首选,只需一条 DDL 语句即可完美解决。MySQL 用户推荐使用**方案二**——用一个 `SlugVersion uint` 字段记录 slug 修改次数,保证同一 slug 不会同时出现在两条未删除记录中。
|
||||
|
||||
> [!warning] 跨数据库行为不一致
|
||||
> MySQL 的 InnoDB 引擎在处理软删除行的唯一约束时存在历史缺陷,不同版本行为可能不同。**不要依赖这种行为一致性**,应在应用层做好校验。
|
||||
|
||||
@@ -211,8 +236,37 @@ result := db.Model(&User{}).
|
||||
fmt.Printf("恢复了 %d 条记录\n", result.RowsAffected)
|
||||
```
|
||||
|
||||
> [!important] 恢复后记得重新设置 UpdatedAt
|
||||
> 恢复操作本身也是一次 UPDATE,所以 `UpdatedAt` 会自动更新。如果你希望保留原始时间戳,需要在更新前保存到临时变量。
|
||||
> [!note] 注意 UpdatedAt 的变化
|
||||
> 恢复操作本质是一次 `UPDATE`,GORM 会自动将 `UpdatedAt` 设为当前时间。如果审计要求记录原始创建信息而非恢复时间,这是正常行为。如需区分「创建」与「恢复」时间,可自行增加 `RestoredAt` 字段。
|
||||
|
||||
## 生命周期钩子
|
||||
|
||||
软删除触发的是一条 UPDATE 语句,因此 **BeforeUpdate / AfterUpdate** 会正常执行:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
func (u *User) BeforeDelete(tx *gorm.DB) error {
|
||||
// db.Delete() 走的是 UPDATE,不会触发 BeforeDelete
|
||||
// 如果需要在此处做逻辑(如级联标记),需用 Unscoped().Delete()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) AfterUpdate(tx *gorm.DB) error {
|
||||
// 软删除发生时,这条钩子会被调用
|
||||
if u.DeletedAt != nil {
|
||||
fmt.Printf("用户 %s 被软删除\n", u.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
> [!warning] BeforeDelete 在软删除时不会被调用
|
||||
> GORM 的默认 `db.Delete()` 只发送 UPDATE SQL,不经过物理删除的钩子链。如果需要在删除前做业务校验(如检查关联记录),可以使用 `Unscoped().Delete()` 或者在应用层自行实现检查逻辑。
|
||||
|
||||
## 定时清理策略
|
||||
|
||||
@@ -236,13 +290,13 @@ func CleanSoftDeletedRecords(tx *gorm.DB) error {
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[执行删除操作] --> TypeQ{业务需求_}
|
||||
TypeQ --> |可恢复/需审计| SoftDel["软删除<br/>db.Delete()"]
|
||||
TypeQ --> |合规要求/不需要恢复| HardDel["物理删除<br/>db.Unscoped().Delete()"]
|
||||
Start["执行删除操作"] --> TypeQ{业务需求}
|
||||
TypeQ --> |可恢复 / 需审计| SoftDel["软删除 db.Delete()"]
|
||||
TypeQ --> |合规要求 / 不需恢复| HardDel["物理删除 Unscoped().Delete()"]
|
||||
|
||||
SoftDel --> AfterSoft[查询时需要已删除数据?]
|
||||
SoftDel --> AfterSoft["查询时需要已删除数据?"]
|
||||
AfterSoft --> |是| UnscopedOn["db.Unscoped()"]
|
||||
AfterSoft --> |否| NormalQ["普通查询<br/>自动过滤"]
|
||||
AfterSoft --> |否| NormalQ["普通查询\n自动过滤"]
|
||||
|
||||
HardDel --> AfterHard["数据永久移除"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user