一、GORM 自定义 SQL 生成的艺术

在实际开发中,我们经常会遇到需要执行复杂 SQL 的场景。GORM 虽然提供了强大的链式调用,但有时候原生 SQL 才是最佳选择。不过直接写原生 SQL 会失去 GORM 的便利性,这时候就需要用到自定义 SQL 生成。

// 技术栈:Golang + GORM + MySQL
// 场景:查询订单及关联用户信息(需要复杂联表条件)

// 定义接收结构体
type OrderWithUser struct {
    OrderID    uint   `gorm:"column:o_id"`
    UserName   string `gorm:"column:u_name"`
    OrderTotal float64
}

// 自定义SQL查询
func GetComplexOrders(db *gorm.DB, minAmount float64) ([]OrderWithUser, error) {
    var results []OrderWithUser
    
    // 使用Raw构建原生SQL,Scan将结果映射到结构体
    err := db.Raw(`
        SELECT 
            o.id AS o_id,
            u.name AS u_name,
            o.total AS order_total
        FROM orders o
        JOIN users u ON o.user_id = u.id
        WHERE o.total > ?
        ORDER BY o.created_at DESC
    `, minAmount).Scan(&results).Error
    
    return results, err
}

技术细节

  1. 使用Raw()执行原生SQL时,GORM会处理参数绑定防止SQL注入
  2. Scan()会自动将结果映射到结构体,注意字段别名要与tag匹配
  3. 复杂查询建议使用明确的AS别名,避免字段冲突

二、预加载策略的深度优化

N+1查询问题是ORM的经典难题。GORM默认的预加载虽然方便,但在处理深层关联或大数据量时性能堪忧。

// 技术栈:Golang + GORM + MySQL
// 优化场景:多层关联查询(用户->订单->商品)

type User struct {
    ID     uint
    Name   string
    Orders []Order `gorm:"foreignKey:UserID"`
}

type Order struct {
    ID       uint
    UserID   uint
    Products []Product `gorm:"many2many:order_products;"`
}

// 传统预加载方式(可能产生性能问题)
db.Preload("Orders").Preload("Orders.Products").Find(&users)

// 优化方案1:使用Join预加载
db.Preload("Orders.Products", func(db *gorm.DB) *gorm.DB {
    return db.Joins("LEFT JOIN order_products ON products.id = order_products.product_id")
}).Find(&users)

// 优化方案2:批量预加载(适用于已知ID范围)
var orderIDs []uint
db.Model(&Order{}).Where("user_id IN ?", userIDs).Pluck("id", &orderIDs)
db.Preload("Products").Where("id IN ?", orderIDs).Find(&orders)

性能对比

  • 传统方式:产生3次查询(用户、订单、商品)
  • Join预加载:2次查询(用户+订单联查、商品联查)
  • 批量预加载:固定2次查询,适合分页场景

三、事务嵌套的实战技巧

分布式事务是复杂系统的痛点,GORM提供了灵活的事务嵌套方案。

// 技术栈:Golang + GORM + MySQL
// 场景:跨服务订单创建(需要本地事务+外部API调用)

func CreateOrderWithPayment(db *gorm.DB, order Order, paymentInfo Payment) error {
    // 开启父事务
    return db.Transaction(func(tx *gorm.DB) error {
        // 创建订单记录
        if err := tx.Create(&order).Error; err != nil {
            return err
        }
        
        // 调用支付服务(外部API)
        if err := callPaymentAPI(paymentInfo); err != nil {
            // 手动回滚标记
            tx.AddError(err)
            return err
        }
        
        // 嵌套事务:更新库存
        if err := tx.Transaction(func(subTx *gorm.DB) error {
            return UpdateInventory(subTx, order.Items)
        }); err != nil {
            return err
        }
        
        return nil
    })
}

// 独立库存服务
func UpdateInventory(db *gorm.DB, items []Item) error {
    for _, item := range items {
        if err := db.Exec(`
            UPDATE products 
            SET stock = stock - ? 
            WHERE id = ? AND stock >= ?
        `, item.Quantity, item.ProductID, item.Quantity).Error; err != nil {
            return err
        }
    }
    return nil
}

关键点

  1. tx.AddError()可将外部系统错误纳入事务管理
  2. 嵌套事务中,子事务失败会触发父事务回滚
  3. 使用Exec直接执行SQL避免模型验证开销

四、高级场景与避坑指南

场景1:分库分表下的ID冲突

// 使用Snowflake ID替代自增ID
type User struct {
    ID int64 `gorm:"primaryKey;autoIncrement:false"`
    // ...
}

// 插入前生成ID
user.ID = snowflake.Generate()
db.Create(&user)

场景2:乐观锁实现

type Product struct {
    ID      uint
    Version int `gorm:"default:1"`
    // ...
}

// 更新时检查版本
db.Model(&product).
    Where("version = ?", product.Version).
    Updates(map[string]interface{}{
        "price": newPrice,
        "version": product.Version + 1,
    })

常见陷阱

  1. 预加载与Select()冲突:同时使用时预加载可能失效
  2. 零值更新问题:使用Updates(map)而非Updates(struct)
  3. 连接泄漏:务必处理rows.Close()

五、技术选型建议

适用场景

  • 需要快速开发的中小型项目
  • 微服务中的单个服务数据层
  • 原型验证阶段

不适用场景

  • 超高频写入的金融系统
  • 需要复杂SQL优化的报表查询
  • 已有成熟DBA团队维护的遗留系统

性能数据(测试环境):

  • 简单查询:GORM比原生SQL慢约15%
  • 复杂联表:合理使用Join预加载可接近原生性能
  • 事务吞吐:嵌套事务比平面事务低约30%