Gin 项目中的 GORM:查询、关联、事务与连接池
GORM 把 SQL 查询结果映射到 Go 结构体,但它没有消除 SQL、事务和连接池。理解 &user、链式查询返回值和关联规则,才能避免“能查到数据但不知道为什么”。
基础模型
1 2 3 4 5 6 7 8 9 10 11 12 13
| type User struct { ID uint Name string Email string Orders []Order }
type Order struct { ID uint UserID uint Amount int64 Status string }
|
GORM 按约定用 Order.UserID 关联 User.ID。约定不符合业务模型时,应显式配置外键,不要依赖猜测。
&user 是什么
1 2
| var user User tx := db.First(&user, 10)
|
&user 是可写地址,GORM 用它接收查询结果;查询条件来自 10,表示按主键查。tx 是本次数据库操作对象,包含 Error 和 RowsAffected 等信息。
1 2 3 4 5 6
| if errors.Is(tx.Error, gorm.ErrRecordNotFound) { return nil } if tx.Error != nil { return tx.Error }
|
查询单条记录为空会返回 ErrRecordNotFound;批量 Find 通常返回空切片而不是这个错误。
多个条件
1 2 3 4 5 6 7
| var users []User tx := db. Where("status = ?", "active"). Where("created_at >= ?", startTime). Order("id DESC"). Limit(20). Find(&users)
|
值应通过参数绑定传入。动态排序字段必须来自白名单,不能直接拼接用户输入。
创建与字段回填
1 2 3 4 5 6 7
| user := User{Name: "Leroi", Email: "leroi@example.com"}
if err := db.Create(&user).Error; err != nil { return err }
fmt.Println(user.ID)
|
主键和时间戳可能由数据库或 GORM 在写入后回填到原结构体,因此需要传指针。
Preload 与 N+1
1 2 3 4
| var users []User err := db. Preload("Orders", "status = ?", "paid"). Find(&users).Error
|
Preload 通常用额外批量查询加载关联,避免循环逐个查订单。它不是自动 Join,仍要查看生成 SQL、结果规模和索引。
事务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| err := db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&order).Error; err != nil { return err }
result := tx.Model(&Product{}). Where("id = ? AND stock > 0", productID). Update("stock", gorm.Expr("stock - 1"))
if result.Error != nil { return result.Error } if result.RowsAffected != 1 { return errors.New("out of stock") } return nil })
|
回调返回错误会回滚。事务里不要调用慢 HTTP 接口,否则会延长持锁和占用连接的时间。
连接池
1 2 3 4 5 6 7 8
| sqlDB, err := db.DB() if err != nil { return err }
sqlDB.SetMaxOpenConns(40) sqlDB.SetMaxIdleConns(10) sqlDB.SetConnMaxLifetime(30 * time.Minute)
|
每个 Goroutine 发起一次查询,就会尝试从连接池获得连接。Goroutine 很轻量不代表数据库连接无限,池大小要和实例数、数据库上限及查询耗时共同计算。
延伸阅读