探索 GORM 软删除:重要特性与应用场景实例

65 阅读8分钟

在实际项目中,软删除是一种非常实用的数据管理技术,它允许我们在不真正删除数据的情况下标记数据为 "已删除",从而保留数据的可追溯性和可恢复性。

这个特性在项目的开发中很常见,也非常重要,因此我单独出了一篇关于软删除的实践案例,以帮助大家更好地理解。

以下是一些使用 GORM 进行软删除的实际案例:

一、电商系统中的商品软删除管理

在电商平台中,商品的上下架管理是核心业务场景。软删除完美适配这一需求,既能实现商品状态管理,又能保留历史销售数据。

1.1 商品模型设计

type Product struct {
    ID          uint           `gorm:"primaryKey"`
    Name        string         `gorm:"not null;index"`             // 商品名称(索引字段)
    Price       float64        `gorm:"not null"`                  // 商品价格
    Description string         `gorm:"type:text"`                 // 商品描述
    Stock       uint           `gorm:"default:0"`                 // 库存数量
    Status      string         `gorm:"default:'active';index"`    // 商品状态(活跃/下架)
    CreatedAt   time.Time                              // 创建时间
    UpdatedAt   time.Time                              // 更新时间
    DeletedAt   gorm.DeletedAt `gorm:"index"`         // 软删除时间戳(索引字段)
}

1.2 核心业务场景实现

1.2.1 商品下架处理

// 商品服务层 - 下架商品
func (s *ProductService) DisableProduct(productID uint) error {
    // 开启事务确保数据一致性
    return s.db.Transaction(func(tx *gorm.DB) error {
        // 1. 查找商品
        var product Product
        if err := tx.First(&product, productID).Error; err != nil {
            return fmt.Errorf("商品不存在: %w", err)
        }
        
        // 2. 检查商品状态(防止重复下架)
        if !product.DeletedAt.IsZero() {
            return errors.New("商品已下架")
        }
        
        // 3. 执行软删除
        if err := tx.Delete(&product).Error; err != nil {
            return fmt.Errorf("下架失败: %w", err)
        }
        
        // 4. 记录操作日志(含操作人信息)
        s.logger.WithFields(zap.Fields{
            "product_id": productID,
            "operator":   s.currentUser.ID,
        }).Info("商品已下架")
        
        return nil
    })
}

1.2.2 商品查询策略

// 查询所有活跃商品(含分页)
func (s *ProductService) GetActiveProducts(page, pageSize int) ([]Product, int64, error) {
    var products []Product
    var total int64
    
    // 构建查询
    query := s.db.Where("status = ? AND deleted_at IS NULL", "active")
    
    // 计算总数
    if err := query.Model(&Product{}).Count(&total).Error; err != nil {
        return nil, 0, err
    }
    
    // 分页查询
    if err := query.
        Offset((page - 1) * pageSize).
        Limit(pageSize).
        Order("created_at DESC").
        Find(&products).Error; err != nil {
        return nil, 0, err
    }
    
    return products, total, nil
}

// 管理员查询所有商品(含已下架)
func (s *ProductService) GetAllProducts() ([]Product, error) {
    var products []Product
    // 使用Unscoped绕过软删除约束
    if err := s.db.Unscoped().Order("deleted_at IS NULL DESC, created_at DESC").Find(&products).Error; err != nil {
        return nil, err
    }
    return products, nil
}

1.2.3 商品恢复机制

// 恢复已下架商品
func (s *ProductService) RestoreProduct(productID uint) error {
    // 1. 查找软删除的商品
    var product Product
    if err := s.db.Unscoped().First(&product, productID).Error; err != nil {
        return fmt.Errorf("商品不存在: %w", err)
    }
    
    // 2. 检查是否已软删除
    if product.DeletedAt.IsZero() {
        return errors.New("商品未下架")
    }
    
    // 3. 恢复商品(设置DeletedAt为NULL)
    if err := s.db.Unscoped().Model(&product).Update("DeletedAt", nil).Error; err != nil {
        return fmt.Errorf("恢复失败: %w", err)
    }
    
    // 4. 重置状态为活跃
    if err := s.db.Model(&product).Update("Status", "active").Error; err != nil {
        return fmt.Errorf("状态重置失败: %w", err)
    }
    
    s.logger.WithField("product_id", productID).Info("商品已恢复")
    return nil
}

二、用户管理系统的账户软删除方案

用户账户管理对数据保留有严格要求,软删除既能满足用户注销需求,又能保留交易记录等历史数据。

2.1 用户模型增强设计

type User struct {
    ID           uint           `gorm:"primaryKey"`
    Username     string         `gorm:"unique;not null;index"`    // 用户名(唯一索引)
    Email        string         `gorm:"unique;not null;index"`    // 邮箱(唯一索引)
    Phone        string         `gorm:"index"`                   // 手机号(索引)
    Status       string         `gorm:"default:'active';index"`   // 账户状态
    LastLoginAt  time.Time                              // 最后登录时间
    CreatedAt    time.Time                              // 创建时间
    UpdatedAt    time.Time                              // 更新时间
    DeletedAt    gorm.DeletedAt `gorm:"index"`          // 软删除时间戳
    // 敏感信息单独处理(如密码加密存储)
    EncryptedPassword string `gorm:"column:password_hash"` // 加密密码
}

2.2 账户生命周期管理

2.2.1 账户注销流程

// 账户服务 - 用户注销
func (s *UserService) CloseAccount(userID uint) error {
    return s.db.Transaction(func(tx *gorm.DB) error {
        // 1. 查找用户
        var user User
        if err := tx.First(&user, userID).Error; err != nil {
            return fmt.Errorf("用户不存在: %w", err)
        }
        
        // 2. 检查账户状态
        if !user.DeletedAt.IsZero() {
            return errors.New("账户已注销")
        }
        
        // 3. 软删除用户(级联处理关联数据)
        if err := tx.Delete(&user).Error; err != nil {
            return fmt.Errorf("注销失败: %w", err)
        }
        
        // 4. 软删除用户订单
        if err := tx.Where("user_id = ?", userID).Delete(&Order{}).Error; err != nil {
            return fmt.Errorf("订单处理失败: %w", err)
        }
        
        // 5. 记录审计日志(含IP等操作信息)
        s.auditLogger.WithFields(zap.Fields{
            "user_id":  userID,
            "operator": s.currentUser.ID,
            "ip":       s.requestIP,
        }).Info("用户账户已注销")
        
        return nil
    })
}

2.2.2 合规性数据清理

// 定期清理过期软删除账户(数据合规需求)
func (s *UserService) PurgeExpiredAccounts(retentionDays int) error {
    // 计算时间阈值(如180天前)
    cutoffTime := time.Now().AddDate(0, 0, -retentionDays)
    
    // 1. 查找所有超过保留期的软删除用户
    var users []User
    if err := s.db.Unscoped().
        Where("deleted_at < ?", cutoffTime).
        Find(&users).Error; err != nil {
        return fmt.Errorf("查询过期账户失败: %w", err)
    }
    
    if len(users) == 0 {
        return nil
    }
    
    // 2. 开启事务进行物理删除
    return s.db.Transaction(func(tx *gorm.DB) error {
        // 2.1 物理删除用户关联数据
        userIDs := make([]uint, len(users))
        for i, user := range users {
            userIDs[i] = user.ID
        }
        
        if err := tx.Unscoped().Where("user_id IN ?", userIDs).Delete(&Order{}).Error; err != nil {
            return fmt.Errorf("删除关联订单失败: %w", err)
        }
        
        // 2.2 物理删除用户(使用批量删除优化性能)
        if err := tx.Unscoped().Delete(&users).Error; err != nil {
            return fmt.Errorf("删除用户失败: %w", err)
        }
        
        // 2.3 记录清理日志
        s.logger.WithFields(zap.Fields{
            "count":      len(users),
            "retention":  retentionDays,
            "deleted_at": cutoffTime,
        }).Info("完成过期账户清理")
        
        return nil
    })
}

2.2.3 账户恢复与数据审查

// 管理员恢复已注销账户
func (s *UserService) RestoreAccount(userID uint) error {
    // 1. 查找软删除的账户
    var user User
    if err := s.db.Unscoped().First(&user, userID).Error; err != nil {
        return fmt.Errorf("账户不存在: %w", err)
    }
    
    // 2. 检查是否已注销
    if user.DeletedAt.IsZero() {
        return errors.New("账户未注销")
    }
    
    // 3. 开启事务恢复账户
    return s.db.Transaction(func(tx *gorm.DB) error {
        // 3.1 恢复用户
        if err := tx.Unscoped().Model(&user).Update("DeletedAt", nil).Error; err != nil {
            return fmt.Errorf("恢复账户失败: %w", err)
        }
        
        // 3.2 恢复用户状态为活跃
        if err := tx.Model(&user).Update("Status", "active").Error; err != nil {
            return fmt.Errorf("更新状态失败: %w", err)
        }
        
        // 3.3 记录操作日志
        s.auditLogger.WithFields(zap.Fields{
            "user_id":  userID,
            "operator": s.currentUser.ID,
        }).Info("已恢复用户账户")
        
        return nil
    })
}

三、内容管理系统的文章软删除实践

在 CMS 系统中,文章的删除与恢复是常见需求,软删除能有效保留内容历史,支持版本追溯。

3.1 文章模型设计

type Article struct {
    ID          uint           `gorm:"primaryKey"`
    Title       string         `gorm:"not null;index"`             // 文章标题(索引)
    Content     string         `gorm:"type:text"`                 // 文章内容
    AuthorID    uint           `gorm:"not null;index"`            // 作者ID(索引)
    CategoryID  uint           `gorm:"index"`                    // 分类ID(索引)
    Status      string         `gorm:"default:'published';index"` // 文章状态
    ViewCount   uint           `gorm:"default:0"`                // 浏览量
    CreatedAt   time.Time                              // 创建时间
    UpdatedAt   time.Time                              // 更新时间
    DeletedAt   gorm.DeletedAt `gorm:"index"`         // 软删除时间戳
    // 多版本支持(简化示例)
    Version     int            `gorm:"default:1"`                 // 版本号
}

3.2 内容生命周期管理

3.2.1 文章软删除实现

// 文章服务 - 删除文章
func (s *ArticleService) DeleteArticle(articleID uint) error {
    return s.db.Transaction(func(tx *gorm.DB) error {
        // 1. 查找文章
        var article Article
        if err := tx.First(&article, articleID).Error; err != nil {
            return fmt.Errorf("文章不存在: %w", err)
        }
        
        // 2. 检查是否已删除
        if !article.DeletedAt.IsZero() {
            return errors.New("文章已删除")
        }
        
        // 3. 执行软删除
        if err := tx.Delete(&article).Error; err != nil {
            return fmt.Errorf("删除失败: %w", err)
        }
        
        // 4. 记录操作日志(含操作类型)
        s.logger.WithFields(zap.Fields{
            "article_id": articleID,
            "operator":   s.currentUser.ID,
            "type":       "soft_delete",
        }).Info("文章已删除")
        
        return nil
    })
}

3.2.2 内容恢复与版本管理

// 恢复已删除文章
func (s *ArticleService) RestoreArticle(articleID uint) error {
    // 1. 查找软删除的文章
    var article Article
    if err := s.db.Unscoped().First(&article, articleID).Error; err != nil {
        return fmt.Errorf("文章不存在: %w", err)
    }
    
    // 2. 检查是否已删除
    if article.DeletedAt.IsZero() {
        return errors.New("文章未删除")
    }
    
    // 3. 恢复文章
    if err := s.db.Transaction(func(tx *gorm.DB) error {
        // 3.1 清除删除标记
        if err := tx.Unscoped().Model(&article).Update("DeletedAt", nil).Error; err != nil {
            return fmt.Errorf("恢复失败: %w", err)
        }
        
        // 3.2 更新状态为发布
        if err := tx.Model(&article).Update("Status", "published").Error; err != nil {
            return fmt.Errorf("状态更新失败: %w", err)
        }
        
        // 3.3 增加版本号
        if err := tx.Model(&article).Update("Version", gorm.Expr("version + 1")).Error; err != nil {
            return fmt.Errorf("版本更新失败: %w", err)
        }
        
        s.logger.WithField("article_id", articleID).Info("文章已恢复")
        return nil
    }); err != nil {
        return err
    }
    
    return nil
}

3.2.3 内容审计与永久删除

// 审核违规文章并永久删除
func (s *ArticleService) PurgeViolatingArticle(articleID uint) error {
    // 1. 查找文章(包括软删除的)
    var article Article
    if err := s.db.Unscoped().First(&article, articleID).Error; err != nil {
        return fmt.Errorf("文章不存在: %w", err)
    }
    
    // 2. 记录审核日志
    auditLog := AuditLog{
        ArticleID:  articleID,
        AuditorID:  s.currentUser.ID,
        Reason:     "content_violation",
        Action:     "permanent_delete",
        CreatedAt:  time.Now(),
    }
    if err := s.db.Create(&auditLog).Error; err != nil {
        return fmt.Errorf("记录审计日志失败: %w", err)
    }
    
    // 3. 物理删除文章
    if err := s.db.Unscoped().Delete(&article).Error; err != nil {
        return fmt.Errorf("永久删除失败: %w", err)
    }
    
    s.auditLogger.WithFields(zap.Fields{
        "article_id": articleID,
        "auditor":    s.currentUser.Username,
        "reason":     "违规内容",
    }).Info("已永久删除违规文章")
    
    return nil
}

四、软删除最佳实践与架构设计

4.1 软删除核心优势总结

  1. 数据可追溯性:保留完整数据历史,支持审计和分析
  2. 操作可逆性:误删除可快速恢复,提升系统容错能力
  3. 业务连续性:下架 / 注销等操作不影响历史数据关联
  4. 合规性支持:满足数据保留法规要求(如 GDPR)

4.2 高级架构设计

4.2.1 软删除全局作用域

// 全局软删除作用域
func SoftDeleteScope(db *gorm.DB) *gorm.DB {
    return db.Where("deleted_at IS NULL")
}

// 应用全局作用域
func SetupSoftDelete(db *gorm.DB) {
    db.Use(func(db *gorm.DB) {
        db.Scopes(SoftDeleteScope)
    })
}

4.2.2 软删除与缓存集成

// 软删除钩子清理缓存
func (u *User) AfterDelete(tx *gorm.DB) error {
    // 清理用户相关缓存
    cacheKey := fmt.Sprintf("user:%d", u.ID)
    if err := cacheClient.Del(cacheKey).Err(); err != nil {
        return fmt.Errorf("清理缓存失败: %w", err)
    }
    
    // 清理用户列表缓存
    if err := cacheClient.Del("users:active").Err(); err != nil {
        return fmt.Errorf("清理用户列表缓存失败: %w", err)
    }
    
    return nil
}

4.3 性能优化要点

  1. 索引策略:在DeletedAt字段创建索引,提升查询性能
  2. 批量操作:使用FindInBatches处理大规模软删除数据
  3. 分表设计:长期软删除数据可迁移至历史表
  4. 查询优化:避免Unscoped滥用,明确需要查询软删除数据时再使用

这些案例展示了软删除在不同业务场景中的应用,包括商品管理、用户账户管理、内容管理等。软删除的核心优势在于它允许我们在不丢失历史数据的情况下管理数据的可见性,同时提供了数据恢复的可能性。在实际应用中,需要根据业务需求合理设计软删除的策略,并注意处理好与软删除相关的查询和事务操作。 

如果这篇文章对大家有帮助可以点赞关注,你的支持就是我的动力😊!