GORM 软删除方案:使用 deleted_at 替代 is_deleted
统一规范,建议时间字段改为xxx_at,deleted_at为gorm 默认认可的删除字段
sql
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
一、问题背景
现状问题
- 使用
is_deleted(0/1)实现软删除 - 唯一索引包含
is_deleted:UNIQUE KEY (merchant_id, sku_no, is_deleted) - 问题:同一业务字段组合的多条已删除记录会违反唯一约束
问题示例
-- 记录1:merchant_id=1, sku_no='SKU001', is_deleted=1 ✅
-- 记录2:merchant_id=1, sku_no='SKU001', is_deleted=1 ❌ 违反唯一约束!
二、解决方案
使用 deleted_at(时间戳)替代 is_deleted,唯一索引包含 deleted_at。
核心原理
- 未删除 :
deleted_at = NULL - 已删除 :
deleted_at = 具体时间 - 唯一索引 :只包含业务字段,不包含
deleted_at
优势
- ✅ 支持唯一索引(已删除记录不影响唯一性)
- ✅ 自动处理(GORM 自动过滤已删除记录)
- ✅ 记录删除时间(
deleted_at保存删除时间) - ✅ 可以恢复(将
deleted_at设置为 NULL)
三、数据库设计
go
CREATE TABLE `merchant_product` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`merchant_id` bigint NOT NULL COMMENT '商家ID',
`sku_no` varchar(32) NOT NULL COMMENT '商品sku唯一编码',
`merchant_product_name` varchar(200) NOT NULL COMMENT '商家自定义商品名称',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted_at` datetime NULL DEFAULT NULL COMMENT '删除时间,NULL表示未删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_merchant_sku` (`merchant_id`, `sku_no`,`deleted_at`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家商品表';## 四、GORM 模型定义
import (
"time"
"gorm.io/gorm"
)
type MerchantProduct struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
MerchantID int64 `gorm:"column:merchant_id;not null;comment:商家ID" json:"merchant_id"` // 商家ID
MerchantNo string `gorm:"column:merchant_no;not null;comment:商家编号" json:"merchant_no"` // 商家编号
SkuNo string `gorm:"column:sku_no;not null;comment:商品sku唯一编码" json:"sku_no"` // 商品sku唯一编码
MerchantProductName string `gorm:"column:merchant_product_name;not null;comment:商家自定义商品名称" json:"merchant_product_name"` // 商家自定义商品名称
MerchantProductImage string `gorm:"column:merchant_product_image;not null;comment:商家自定义商品图片" json:"merchant_product_image"` // 商家自定义商品图片
BarCode string `gorm:"column:bar_code;comment:条形编码" json:"bar_code"` // 条形编码
BarCodePicture string `gorm:"column:bar_code_picture;not null;comment:条形码图片" json:"bar_code_picture"` // 条形码图片
IsStandard int32 `gorm:"column:is_standard;not null;comment:是否是标品0否1是" json:"is_standard"` // 是否是标品0否1是
ReferencePrice decimal.Decimal `gorm:"column:reference_price;not null;default:0.00;comment:商品参考售价" json:"reference_price"` // 商品参考售价
CostPrice decimal.Decimal `gorm:"column:cost_price;not null;default:0.00;comment:成本价" json:"cost_price"` // 成本价
PriceUnit string `gorm:"column:price_unit;not null;comment:价格单位" json:"price_unit"` // 价格单位
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间" json:"deleted_at"` // 删除时间
}
## 五、代码使用指南
### 1. 软删除
// 方法1:使用 Delete()(推荐,GORM 自动处理)
product := &MerchantProduct{ID: 1}
err := db.Delete(product).Error
// SQL: UPDATE merchant_product SET deleted_at = NOW() WHERE id = 1 AND deleted_at IS NULL
// 方法2:批量软删除
err := db.Where("merchant_id = ?", 1).Delete(&MerchantProduct{}).Error### 2. 真删除(永久删除)
// 使用 Unscoped() 绕过软删除机制
err := db.Unscoped().Delete(&MerchantProduct{ID: 1}).Error
// SQL: DELETE FROM merchant_product WHERE id = 1
// 批量真删除
err := db.Unscoped().Where("merchant_id = ?", 1).Delete(&MerchantProduct{}).Error### 3. 更新数据(不触发软删除)
// 正常更新业务字段,不会影响 deleted_at
err := db.Model(&MerchantProduct{}).
Where("id = ?", 1).
Update("merchant_product_name", "新商品名称").Error
// SQL: UPDATE merchant_product SET merchant_product_name = '新商品名称' WHERE id = 1
// deleted_at 字段不会被修改
// 批量更新
err := db.Model(&MerchantProduct{}).
Where("merchant_id = ?", 1).
Updates(map[string]interface{}{
"merchant_product_name": "新名称",
"reference_price": 99.99,
}).Error### 4. 更新数据 + 同时软删除
// 方法1:一次性更新(推荐)
err := db.Unscoped().Model(&MerchantProduct{}).
Where("id = ?", 1).
Updates(map[string]interface{}{
"merchant_product_name": "新名称",
"reference_price": 99.99,
"deleted_at": time.Now(), // 同时软删除
}).Error
#### 5.1 默认查询(自动过滤已删除记录)
// 默认查询:自动过滤已删除的记录(推荐)
var products []MerchantProduct
err := db.Where("merchant_id = ?", 1).Find(&products).Error
// SQL: SELECT * FROM merchant_product WHERE merchant_id = 1 AND deleted_at IS NULL
// ✅ 不需要手动加 deleted_at IS NULL 条件
// 查询单条
var product MerchantProduct
err := db.First(&product, 1).Error
// 如果记录已删除,会返回 gorm.ErrRecordNotFound#### 5.2 查询所有记录(包括已删除的)
// 使用 Unscoped() 查询所有记录
var allProducts []MerchantProduct
err := db.Unscoped().Where("merchant_id = ?", 1).Find(&allProducts).Error
// SQL: SELECT * FROM merchant_product WHERE merchant_id = 1#### 5.3 只查询已删除的记录
// 只查询已删除的记录
var deletedProducts []MerchantProduct
err := db.Unscoped().
Where("merchant_id = ? AND deleted_at IS NOT NULL", 1).
Find(&deletedProducts).Error### 6. 恢复已删除的记录
// 恢复记录(将 deleted_at 设置为 NULL)
err := db.Unscoped().Model(&MerchantProduct{}).
Where("id = ?", 1).
Update("deleted_at", nil).Error
// SQL: UPDATE merchant_product SET deleted_at = NULL WHERE id = 1## 六、关键点总结
| 操作 | 方法 | 说明 |
|---|---|---|
| 软删除 | db.Delete(&product) |
GORM 自动设置 deleted_at = NOW() |
| 真删除 | db.Unscoped().Delete(&product) |
永久删除记录 |
| 正常更新 | db.Update() / db.Updates() |
不会影响 deleted_at |
| 更新+删除 | db.Unscoped().Updates() |
同时更新字段和 deleted_at |
| 默认查询 | db.Find() |
自动过滤已删除记录 |
| 查询全部 | db.Unscoped().Find() |
包含已删除记录 |
| 恢复记录 | db.Unscoped().Update("deleted_at", nil) |
恢复已删除记录 |
七、注意事项
- 唯一索引 :不包含
deleted_at,只包含业务字段 - 默认行为 :有
DeletedAt字段时,Delete()是软删除,查询自动过滤 - 真删除 :必须使用
Unscoped().Delete() - 更新已删除记录 :使用
Unscoped()才能更新到已删除的记录
方案2:
is_deleted字段保留改字段 同时需要创建唯一索引时,把该字段追加到唯一索引下, 保存的时候保存为主键id,但是就有点语义不清晰, 且批量删除的时候会有问题,比如UPDATE merchant_product SET is_deleted=? WHERE merchant_id=1
不建议使用