GORM 软删除方案:使用 deleted_at 替代 is_deleted,用来支持表唯一索引创建

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_deletedUNIQUE 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) 恢复已删除记录

七、注意事项

  1. 唯一索引 :不包含 deleted_at,只包含业务字段
  2. 默认行为 :有 DeletedAt 字段时,Delete() 是软删除,查询自动过滤
  3. 真删除 :必须使用 Unscoped().Delete()
  4. 更新已删除记录 :使用 Unscoped() 才能更新到已删除的记录

方案2:

is_deleted字段保留改字段 同时需要创建唯一索引时,把该字段追加到唯一索引下, 保存的时候保存为主键id,但是就有点语义不清晰, 且批量删除的时候会有问题,比如UPDATE merchant_product SET is_deleted=? WHERE merchant_id=1

不建议使用

相关推荐
R.lin2 小时前
Spring AI Alibaba 1.1 正式发布!
java·后端·spring
程序员阿明2 小时前
spring security 6的知识点总结
java·后端·spring
running up3 小时前
Spring Bean生命周期- BeanDefinition 加载与 BeanFactoryPostProcessor BeanPostProcessor
java·后端·spring
云上漫步者3 小时前
深度实战:Rust交叉编译适配OpenHarmony PC——unicode_width完整适配案例
开发语言·后端·rust·harmonyos
Java水解3 小时前
MySQL必备基础
后端·mysql
Java水解3 小时前
Spring AOP原理深度解析:代理模式、JDK动态代理与CGLIB
后端·spring
无限大63 小时前
为什么显示器分辨率越高越清晰?——从像素到 4K/8K 的视觉革命
后端
阿苟4 小时前
nginx部署踩坑
前端·后端
ChineHe4 小时前
Gin框架基础篇001_路由与路由组详解
后端·golang·gin