常见问题与最佳实践


第十四章:常见问题与最佳实践

14.1 错误处理

常见错误类型

import (
    "errors"
    "gorm.io/gorm"
)

// 记录不存在
err := db.First(&user, 100).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
    // 处理记录不存在
}

// 重复键错误(MySQL)
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
    if mysqlErr.Number == 1062 {
        // 唯一约束冲突
    }
}

// 外键约束错误
if errors.Is(err, gorm.ErrForeignKeyViolated) {
    // 外键约束违反
}

// 约束验证错误
if errors.Is(err, gorm.ErrCheckConstraintViolated) {
    // 检查约束违反
}

错误处理最佳实践

type DatabaseError struct {
    Code    string
    Message string
    Err     error
}

func (e *DatabaseError) Error() string {
    return e.Message
}

func WrapError(err error) error {
    if err == nil {
        return nil
    }
    
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return &DatabaseError{Code: "NOT_FOUND", Message: "记录不存在", Err: err}
    }
    
    // 其他错误处理...
    return &DatabaseError{Code: "INTERNAL", Message: "数据库错误", Err: err}
}

14.2 调试技巧

打印 SQL

// 方式1:全局日志
db.Logger = logger.Default.LogMode(logger.Info)

// 方式2:会话级别
db.Session(&gorm.Session{
    Logger: logger.Default.LogMode(logger.Info),
}).Find(&users)

// 方式3:生成 SQL 不执行
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement
fmt.Println(stmt.SQL.String())
fmt.Println(stmt.Vars)

慢查询日志

db.Logger = logger.New(
    log.New(os.Stdout, "\r\n", log.LstdFlags),
    logger.Config{
        SlowThreshold: 100 * time.Millisecond,
        LogLevel:      logger.Warn,
        Colorful:      true,
    },
)

Explain 分析

// MySQL
var result []map[string]interface{}
db.Raw("EXPLAIN ?", db.ToSQL(func(tx *gorm.DB) *gorm.DB {
    return tx.Find(&users)
})).Scan(&result)

14.3 性能陷阱

1. N+1 查询

// 错误
db.Find(&users)
for _, u := range users {
    db.Model(&u).Association("Orders").Find(&u.Orders)
}

// 正确
db.Preload("Orders").Find(&users)

2. 大表全表扫描

// 错误:无索引查询
db.Where("content LIKE ?", "%keyword%").Find(&articles)

// 正确:使用全文索引或其他方案
db.Where("MATCH(title, content) AGAINST(?)", "keyword").Find(&articles)

3. 大结果集处理

// 错误:一次性加载所有数据
var allUsers []User
db.Find(&allUsers)  // 百万级数据会 OOM

// 正确1:分批处理
var users []User
db.FindInBatches(&users, 1000, func(tx *gorm.DB, batch int) error {
    for _, user := range users {
        // 处理
    }
    return nil
})

// 正确2:游标处理
rows, err := db.Model(&User{}).Rows()
for rows.Next() {
    var user User
    db.ScanRows(rows, &user)
    // 处理
}

4. 连接泄漏

// 错误:没有正确关闭
func BadQuery() {
    db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    // 使用完后 db 没有被关闭
}

// 正确
goodDB, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
defer func() {
    sqlDB, _ := goodDB.DB()
    sqlDB.Close()
}()

14.4 最佳实践

1. 模型设计

// 使用合适的数据类型
type GoodModel struct {
    ID        uint64    `gorm:"primaryKey"`  // 大表使用 uint64
    CreatedAt time.Time `gorm:"index"`       // 常用查询字段加索引
    Status    int8      `gorm:"index"`       // 状态字段加索引
    Data      string    `gorm:"type:text"`   // 大文本用 text
    JSONData  datatypes.JSON  // JSON 数据
}

// 避免:在常用查询字段上使用 null
// 推荐:使用默认值

2. 查询规范

// 1. 总是限制返回数量
db.Limit(100).Find(&users)

// 2. 只查询需要的字段
db.Select("id", "name").Find(&users)

// 3. 使用上下文控制超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
db.WithContext(ctx).Find(&users)

// 4. 批量操作使用事务
db.Transaction(func(tx *gorm.DB) error {
    for _, item := range items {
        if err := tx.Create(&item).Error; err != nil {
            return err
        }
    }
    return nil
})

3. 连接池配置

sqlDB, _ := db.DB()

// 根据服务器配置调整
sqlDB.SetMaxIdleConns(25)
sqlDB.SetMaxOpenConns(50)
sqlDB.SetConnMaxLifetime(30 * time.Minute)

4. 安全规范

// 1. 防止 SQL 注入
// 正确:使用参数化查询
db.Where("name = ?", userInput).Find(&users)

// 错误:字符串拼接
db.Where("name = '" + userInput + "'").Find(&users)  // SQL 注入风险

// 2. 敏感字段加密
type User struct {
    Password string `json:"-" gorm:"->:false"`  // 不返回,只写入
}

// 3. 软删除替代硬删除
type Model struct {
    DeletedAt gorm.DeletedAt `gorm:"index"`
}

14.5 常见场景解决方案

软删除恢复

// 恢复软删除的记录
db.Unscoped().Model(&user).Update("deleted_at", nil)

去重查询

// 查询某个字段的所有不重复值
db.Model(&User{}).Distinct("country").Pluck("country", &countries)

随机排序

// MySQL
db.Order("RAND()").Limit(10).Find(&users)

// PostgreSQL
db.Order("RANDOM()").Limit(10).Find(&users)

时间范围查询

// 今天
today := time.Now().Format("2006-01-02")
db.Where("DATE(created_at) = ?", today).Find(&orders)

// 最近7天
weekAgo := time.Now().AddDate(0, 0, -7)
db.Where("created_at > ?", weekAgo).Find(&orders)

// 某月
db.Where("YEAR(created_at) = ? AND MONTH(created_at) = ?", 2024, 1).Find(&orders)

树形结构查询

// 递归 CTE 查询(MySQL 8.0+ / PostgreSQL)
type Category struct {
    ID       uint
    Name     string
    ParentID *uint
    Children []Category `gorm:"-" json:"children,omitempty"`
}

func GetCategoryTree(db *gorm.DB, parentID *uint) ([]Category, error) {
    var categories []Category
    err := db.Where("parent_id = ?", parentID).Find(&categories).Error
    if err != nil {
        return nil, err
    }
    
    for i := range categories {
        children, err := GetCategoryTree(db, &categories[i].ID)
        if err != nil {
            return nil, err
        }
        categories[i].Children = children
    }
    
    return categories, nil
}

14.6 迁移与部署

生产环境迁移策略

// 1. 只运行一次迁移
// 使用 golang-migrate 或类似工具

// 2. 应用程序启动时检查而非自动迁移
func CheckSchema(db *gorm.DB) error {
    // 检查关键表是否存在
    if !db.Migrator().HasTable(&User{}) {
        return errors.New("用户表不存在,请先运行迁移")
    }
    return nil
}

// 3. 蓝绿部署时数据库兼容
// - 先添加新列(可空)
// - 双写新旧字段
// - 数据回填
// - 切读流量
// - 删除旧字段

14.7 调试检查清单

// 遇到问题时的检查步骤:

// 1. 查看生成的 SQL
// db = db.Debug()

// 2. 检查连接池状态
// stats := db.DB().Stats()

// 3. 检查是否使用索引
// EXPLAIN 查询

// 4. 检查事务是否提交
// 确保 Commit() 被调用

// 5. 检查软删除
// 使用 Unscoped() 查看被删除的数据

// 6. 检查钩子执行
// 添加日志或断点

14.8 练习题

  1. 排查一个线上出现的 N+1 查询问题
  2. 优化一个执行缓慢的报表查询(超过 10 秒)
  3. 设计一个支持分表的用户系统迁移方案

14.9 小结

本章总结了 GORM 使用中的常见问题、调试技巧和最佳实践。掌握这些内容可以帮助你更高效地使用 GORM,避免常见陷阱。


本文代码地址:https://github.com/LittleMoreInteresting/gorm_study

欢迎关注公众号,一起学习进步!

如有疑问关注公众号给我留言
wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo