常见问题与最佳实践
第十四章:常见问题与最佳实践
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 练习题
- 排查一个线上出现的 N+1 查询问题
- 优化一个执行缓慢的报表查询(超过 10 秒)
- 设计一个支持分表的用户系统迁移方案
14.9 小结
本章总结了 GORM 使用中的常见问题、调试技巧和最佳实践。掌握这些内容可以帮助你更高效地使用 GORM,避免常见陷阱。
本文代码地址:https://github.com/LittleMoreInteresting/gorm_study
欢迎关注公众号,一起学习进步!