GORM 软删除方案:使用 deleted_at 替代 is_deleted
统一规范,建议时间字段改为 xxx_at,deleted_at 为 gorm 默认认可的删除字段。
注意:deleted_at 使用 bigint 类型存储毫秒时间戳,默认值为 0 表示未删除。
公共表结构规范
CREATE TABLE `common` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
说明:
deleted_at字段使用 毫秒时间戳 存储,0表示未删除,非零值为删除时的毫秒时间戳。
一、问题背景
现状问题
- 使用
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 = 0 - 已删除:
deleted_at = 毫秒时间戳(如:1703318400000) - 唯一索引:包含业务字段 +
deleted_at
为什么不用 datetime 类型?
关键问题:MySQL 中 NULL != NULL,导致唯一索引失效!
如果使用 datetime NULL DEFAULT NULL 方案:
- 未删除记录:
deleted_at = NULL - 唯一索引:
UNIQUE KEY (merchant_id, sku_no, deleted_at)
-- 问题示例:
INSERT INTO merchant_product (merchant_id, sku_no, deleted_at) VALUES (1, 'SKU001', NULL); -- ✅
INSERT INTO merchant_product (merchant_id, sku_no, deleted_at) VALUES (1, 'SKU001', NULL); -- ✅ 也能插入!
-- 原因:在 MySQL 中 NULL != NULL,所以两条 deleted_at = NULL 的记录被认为是不同的
-- 唯一索引无法约束 NULL 值,导致同一业务字段组合可以插入多条"未删除"的记录!
使用 bigint 毫秒时间戳方案解决此问题:
- 未删除记录:
deleted_at = 0(确定的值,可被唯一索引约束) - 已删除记录:
deleted_at = 毫秒时间戳(每次删除时间不同,不会冲突)
-- 正确示例:
INSERT INTO merchant_product (merchant_id, sku_no, deleted_at) VALUES (1, 'SKU001', 0); -- ✅
INSERT INTO merchant_product (merchant_id, sku_no, deleted_at) VALUES (1, 'SKU001', 0); -- ❌ 违反唯一约束!
-- deleted_at = 0 是确定的值,唯一索引可以正常工作
优势
- ✅ 唯一索引有效(用 0 替代 NULL,解决 NULL != NULL 问题)
- ✅ 支持多次删除(每次删除的毫秒时间戳不同,不会冲突)
- ✅ 自动处理(GORM 自动过滤已删除记录)
- ✅ 记录删除时间(
deleted_at保存删除时间的毫秒数) - ✅ 可以恢复(将
deleted_at设置为 0) - ✅ 高性能(bigint 比 datetime 索引更高效)
三、数据库设计
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 NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_merchant_sku` (`merchant_id`, `sku_no`, `deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家商品表';
说明:
deleted_at使用毫秒时间戳,0表示未删除。
四、gorm_gen 代码生成配置
使用 gorm_gen 自动生成模型代码,关键配置如下:
package main
import (
"log"
"github.com/huandu/xstrings"
"github.com/shopspring/decimal"
"gorm.io/driver/mysql"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
)
var (
_ = decimal.New(1, 1)
_ = soft_delete.DeletedAt(0)
)
func main() {
g := gen.NewGenerator(gen.Config{
OutPath: "./internal/dal/query",
ModelPkgPath: "./internal/dal/model",
FieldNullable: true,
Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface,
})
db, _ := gorm.Open(mysql.Open("root:123456@(127.0.0.1:3306)/hh_mms?charset=utf8mb4&parseTime=True&loc=Local"))
g.UseDB(db)
sqlDB, _ := db.DB()
if err := sqlDB.Ping(); err != nil {
log.Fatal(err)
return
}
// 处理表名以 s 结尾被截掉问题
g.WithModelNameStrategy(func(tableName string) (modelName string) {
return xstrings.ToPascalCase(tableName)
})
// 自定义数据类型映射
g.WithDataTypeMap(map[string]func(columnType gorm.ColumnType) (dataType string){
"tinyint": func(columnType gorm.ColumnType) string { return "int32" },
"decimal": func(columnType gorm.ColumnType) string { return "decimal.Decimal" },
"bigint": func(columnType gorm.ColumnType) string {
// deleted_at 字段使用 soft_delete.DeletedAt 类型
if columnType.Name() == "deleted_at" {
return "soft_delete.DeletedAt"
}
return "int64"
},
})
// 为 deleted_at 字段添加 softDelete:milli tag(milli 表示毫秒时间戳)
g.WithOpts(
gen.FieldGORMTag("deleted_at", func(tag field.GormTag) field.GormTag {
tag.Set("softDelete", "milli")
return tag
}),
)
// 生成模型
g.ApplyBasic(
g.GenerateModel("mms_device"),
g.GenerateModel("mms_device_relation"),
// ... 其他表
)
g.Execute()
}
关键配置说明
| 配置项 | 说明 |
|---|---|
WithDataTypeMap | 将 deleted_at 字段的 bigint 类型映射为 soft_delete.DeletedAt |
WithOpts + FieldGORMTag | 为 deleted_at 字段添加 softDelete:"milli" tag,milli 表示使用毫秒时间戳 |
五、生成的 GORM 模型示例
import (
"time"
"gorm.io/plugin/soft_delete"
"github.com/shopspring/decimal"
)
type MerchantProduct struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"`
MerchantID int64 `gorm:"column:merchant_id;not null;comment:商家ID" json:"merchant_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"`
MerchantProductName string `gorm:"column:merchant_product_name;not null;comment:商家自定义商品名称" json:"merchant_product_name"`
ReferencePrice decimal.Decimal `gorm:"column:reference_price;not null;default:0.00;comment:商品参考售价" json:"reference_price"`
CreatedAt time.Time `gorm:"column:created_at;not null;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;comment:更新时间" json:"updated_at"`
DeletedAt soft_delete.DeletedAt `gorm:"column:deleted_at;not null;default:0;softDelete:milli;comment:删除时间" json:"deleted_at"`
}
关键字段说明
DeletedAt soft_delete.DeletedAt:使用 gorm 的 soft_delete 插件类型softDelete:milli:表示使用毫秒时间戳存储删除时间default:0:未删除时默认值为 0
六、代码使用指南
1. 软删除
// 方法1:使用 Delete()(推荐,GORM 自动处理)
product := &MerchantProduct{ID: 1}
err := db.Delete(product).Error
// SQL: UPDATE merchant_product SET deleted_at = 1703318400000 WHERE id = 1 AND deleted_at = 0
// 注:1703318400000 为毫秒时间戳
// 方法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 AND deleted_at = 0
4. 查询数据
4.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 = 0
// ✅ 不需要手动加 deleted_at = 0 条件
// 查询单条
var product MerchantProduct
err := db.First(&product, 1).Error
// 如果记录已删除,会返回 gorm.ErrRecordNotFound
4.2 查询所有记录(包括已删除的)
// 使用 Unscoped() 查询所有记录
var allProducts []MerchantProduct
err := db.Unscoped().Where("merchant_id = ?", 1).Find(&allProducts).Error
// SQL: SELECT * FROM merchant_product WHERE merchant_id = 1
4.3 只查询已删除的记录
// 只查询已删除的记录
var deletedProducts []MerchantProduct
err := db.Unscoped().
Where("merchant_id = ? AND deleted_at > 0", 1).
Find(&deletedProducts).Error
5. 恢复已删除的记录
// 恢复记录(将 deleted_at 设置为 0)
err := db.Unscoped().Model(&MerchantProduct{}).
Where("id = ?", 1).
Update("deleted_at", 0).Error
// SQL: UPDATE merchant_product SET deleted_at = 0 WHERE id = 1
七、关键点总结
| 操作 | 方法 | 说明 |
|---|---|---|
| 软删除 | db.Delete(&product) | GORM 自动设置 deleted_at = 毫秒时间戳 |
| 真删除 | db.Unscoped().Delete(&product) | 永久删除记录 |
| 正常更新 | db.Update() / db.Updates() | 不会影响 deleted_at |
| 默认查询 | db.Find() | 自动过滤 deleted_at > 0 的记录 |
| 查询全部 | db.Unscoped().Find() | 包含已删除记录 |
| 恢复记录 | db.Unscoped().Update("deleted_at", 0) | 恢复已删除记录 |
八、与 datetime 方案对比
| 特性 | bigint 毫秒时间戳方案(推荐) | datetime 方案 |
|---|---|---|
| 字段类型 | bigint unsigned NOT NULL DEFAULT '0' | datetime NULL DEFAULT NULL |
| 未删除值 | 0 | NULL |
| 已删除值 | 毫秒时间戳(如:1703318400000) | 具体时间(如:2023-12-23 12:00:00) |
| 唯一索引 | ✅ 有效(0 是确定值) | ❌ 失效(NULL != NULL) |
| 索引性能 | 更高(bigint 比较更快) | 一般 |
| GORM 类型 | soft_delete.DeletedAt | gorm.DeletedAt |
| gorm tag | softDelete:milli | 无需额外 tag |
九、注意事项
-
唯一索引:包含
deleted_at字段,用 0 替代 NULL 解决唯一索引失效问题 -
默认行为:有
soft_delete.DeletedAt字段时,Delete()是软删除,查询自动过滤 -
真删除:必须使用
Unscoped().Delete() -
更新已删除记录:使用
Unscoped()才能更新到已删除的记录 -
依赖包:需要引入
gorm.io/plugin/soft_delete -
时间戳单位:
softDelete:milli表示毫秒,如需秒级可使用softDelete:unix