db.delete 默认按主键删单条记录,传结构体时仅主键生效;批量删除须用 db.where().delete(),软删除需注意 deletedat 字段影响,物理删除必须加 unscoped(),且大表应分批执行以防锁表。

db.Delete 不适合批量删除,它默认只删单条且依赖主键;真要批量删,得用 db.Where(...).Delete(&Model{}) 或更稳妥的 db.Unscoped().Where(...).Delete(&Model{}),但每种写法背后行为差异很大,稍不注意就删错或删不掉。
为什么 Delete 带结构体参数会出问题
当你写 db.Delete(&User{ID: 1}),GORM 实际只取 ID 字段构造 WHERE 条件,其他字段全被忽略。如果传的是 db.Delete(&[]User{{ID: 1}, {ID: 2}}),新版 GORM(v2)会直接 panic,老版(v1)则可能误判为“按主键批量删”,但行为不可靠、无文档保证。
- 结构体切片传给
Delete是未定义行为,别这么干 - 哪怕只传一个带非主键字段的结构体,比如
&User{ID: 1, Name: "foo"},Name也不会进 WHERE - 真正生效的永远只有主键字段(或你显式指定的
clause.PrimaryKey)
Where + Delete 是标准做法,但要注意软删除
默认情况下,db.Where("status = ?", "inactive").Delete(&User{}) 实际执行的是软删除(即 SET deleted_at = NOW()),前提是你的模型嵌入了 gorm.Model 或定义了 DeletedAt 字段。这在多数业务场景下是预期行为,但如果你真要物理删,必须显式绕过。
- 软删除:直接
db.Where(...).Delete(&User{}) - 物理删除:加
Unscoped(),即db.Unscoped().Where(...).Delete(&User{}) - 想删前确认影响行数?先
db.Where(...).Select("id").Find(&[]User{})查 ID 列表,再决定是否删
WHERE 条件写错会导致误删或零影响
常见错误是把字符串条件写成结构体条件,比如 db.Where(User{Status: "inactive"}).Delete(&User{})。GORM 会把整个结构体转成 AND 条件,但若字段没加 tag 或类型不匹配(如 Status 是 int 但传了 string),可能生成空 WHERE 或隐式类型转换失败,最终删掉全部或删不掉任何记录。
- 永远优先用
db.Where("status = ?", "inactive")这类表达式式写法 - 避免
db.Where(User{...}),除非你明确知道每个字段的 DB 映射和非空性 - 涉及时间范围时,注意时区——
db.Where("created_at > ?", time.Now().AddDate(0,0,-30))比字符串拼接安全得多
大表批量删要防锁表和超时
直接 db.Unscoped().Where("id > ?", 10000).Delete(&User{}) 在百万级表上可能锁住整个表几秒,甚至触发 MySQL 的 lock_wait_timeout。生产环境必须分批删。
- 用
db.Unscoped().Where(...).Limit(1000).Delete(&User{})控制单次数量 - 循环执行,每次检查
result.RowsAffected是否为 0 来退出 - 别依赖
SELECT COUNT(*)预估总量——InnoDB 下这个本身也慢,且可能和 DELETE 并发不一致
db.Session(&gorm.Session{DryRun: true}).Where(...).Delete(&User{}) 打印 SQL 看 WHERE 是否符合预期,比凭经验猜靠谱得多。软删和物理删的语义边界一旦模糊,恢复数据的成本远高于多写一行 Unscoped()。











