Go 数据库操作实战——GORM 进阶、事务、软删除、关联查询优化
大约 6 分钟
Go 数据库操作实战——GORM 进阶、事务、软删除、关联查询优化
适读人群:Go后端开发者、用GORM做项目的工程师 | 阅读时长:约20分钟 | 核心价值:GORM用起来简单,但用好需要理解它的坑,尤其是N+1查询和事务处理
一、小胡的「N+1查询」噩梦
小胡接手了一个电商项目,有个接口是获取订单列表,包含每个订单的商品详情。他写了这样的代码:
func GetOrdersWithItems(userID int64) ([]*Order, error) {
var orders []*Order
db.Where("user_id = ?", userID).Find(&orders)
for _, order := range orders {
db.Where("order_id = ?", order.ID).Find(&order.Items)
}
return orders, nil
}功能完全正确。但接口响应时间随着订单数量线性增长——用户有10个订单,就发11条SQL;有100个订单,就发101条SQL。数据库压力巨大。
这就是臭名昭著的N+1查询问题。Spring里的JPA/Hibernate会自动做懒加载,很多Java工程师初次看到这个问题时都觉得陌生——因为Spring帮你藏起来了,同样的问题只是被推迟暴露。GORM默认不做预加载,你必须显式告诉它。
二、GORM基础配置:从连接到日志
package main
import (
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func NewDB(dsn string) (*gorm.DB, error) {
// 自定义日志:开发环境打印所有SQL,生产环境只打印慢查询
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 200 * time.Millisecond, // 慢查询阈值
LogLevel: logger.Warn, // 生产用Warn,开发用Info
IgnoreRecordNotFoundError: true, // 忽略RecordNotFound错误
Colorful: true,
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
PrepareStmt: true, // 预处理语句,提升性能
DisableAutomaticPing: false,
})
if err != nil {
return nil, err
}
// 配置连接池
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(100) // 最大连接数
sqlDB.SetMaxIdleConns(10) // 最大空闲连接
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
return db, nil
}三、模型定义:充分利用GORM标签
package model
import (
"time"
"gorm.io/gorm"
)
// BaseModel:基础字段,所有模型嵌入
type BaseModel struct {
ID uint `gorm:"primaryKey;autoIncrement"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除字段
}
type User struct {
BaseModel
Username string `gorm:"uniqueIndex;size:50;not null"`
Email string `gorm:"uniqueIndex;size:100;not null"`
Password string `gorm:"size:100;not null"`
Role string `gorm:"size:20;default:user"`
Orders []Order `gorm:"foreignKey:UserID"` // 一对多关联
}
type Order struct {
BaseModel
UserID uint `gorm:"index;not null"`
Status string `gorm:"size:20;default:pending"`
TotalAmt float64 `gorm:"type:decimal(10,2)"`
User User `gorm:"foreignKey:UserID"` // 属于
Items []OrderItem `gorm:"foreignKey:OrderID"`
}
type OrderItem struct {
BaseModel
OrderID uint `gorm:"index;not null"`
ProductID uint `gorm:"index;not null"`
Quantity int `gorm:"not null"`
Price float64 `gorm:"type:decimal(10,2)"`
Product Product `gorm:"foreignKey:ProductID"`
}
type Product struct {
BaseModel
Name string `gorm:"size:200;not null"`
Price float64 `gorm:"type:decimal(10,2)"`
Stock int `gorm:"default:0"`
}四、解决N+1查询:Preload的正确用法
package main
import (
"fmt"
"gorm.io/gorm"
"your-project/model"
)
// 错误:N+1查询
func getOrdersBad(db *gorm.DB, userID uint) ([]*model.Order, error) {
var orders []*model.Order
db.Where("user_id = ?", userID).Find(&orders)
for _, order := range orders {
db.Where("order_id = ?", order.ID).Find(&order.Items) // N次额外查询!
}
return orders, nil
}
// 正确:Preload预加载,只发2条SQL
func getOrdersWithItems(db *gorm.DB, userID uint) ([]*model.Order, error) {
var orders []*model.Order
result := db.
Where("user_id = ?", userID).
Preload("Items"). // 预加载Items,一次批量查询
Preload("Items.Product"). // 预加载Items的Product(三层关联)
Find(&orders)
return orders, result.Error
}
// 更精细控制:Preload时加条件
func getOrdersWithActiveItems(db *gorm.DB, userID uint) ([]*model.Order, error) {
var orders []*model.Order
result := db.
Where("user_id = ?", userID).
Preload("Items", func(db *gorm.DB) *gorm.DB {
return db.Where("quantity > ?", 0).Order("id ASC")
}).
Find(&orders)
return orders, result.Error
}
// Joins:JOIN查询(可以在WHERE里过滤关联条件)
func getOrdersJoinUser(db *gorm.DB) ([]*model.Order, error) {
var orders []*model.Order
result := db.
Joins("User"). // JOIN users表,加载User对象
Where("users.role = ?", "vip").
Find(&orders)
return orders, result.Error
}
func main() {
fmt.Println("示例代码,需要配合实际数据库使用")
}五、事务处理:数据一致性的保障
package main
import (
"errors"
"fmt"
"gorm.io/gorm"
"your-project/model"
)
// 方案1:手动事务
func createOrderManualTx(db *gorm.DB, userID uint, items []model.OrderItem) error {
tx := db.Begin()
if tx.Error != nil {
return tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 创建订单
order := &model.Order{
UserID: userID,
Status: "pending",
}
if err := tx.Create(order).Error; err != nil {
tx.Rollback()
return fmt.Errorf("创建订单失败: %w", err)
}
// 创建订单项,同时扣减库存
for i := range items {
items[i].OrderID = order.ID
// 扣减库存(使用乐观锁:WHERE stock >= quantity)
result := tx.Model(&model.Product{}).
Where("id = ? AND stock >= ?", items[i].ProductID, items[i].Quantity).
UpdateColumn("stock", gorm.Expr("stock - ?", items[i].Quantity))
if result.Error != nil {
tx.Rollback()
return fmt.Errorf("扣减库存失败: %w", result.Error)
}
if result.RowsAffected == 0 {
tx.Rollback()
return errors.New("库存不足")
}
if err := tx.Create(&items[i]).Error; err != nil {
tx.Rollback()
return fmt.Errorf("创建订单项失败: %w", err)
}
}
return tx.Commit().Error
}
// 方案2:Transaction闭包(推荐,自动处理Commit/Rollback)
func createOrderTx(db *gorm.DB, userID uint, items []model.OrderItem) error {
return db.Transaction(func(tx *gorm.DB) error {
order := &model.Order{
UserID: userID,
Status: "pending",
}
if err := tx.Create(order).Error; err != nil {
return err // 返回error自动rollback
}
for i := range items {
items[i].OrderID = order.ID
result := tx.Model(&model.Product{}).
Where("id = ? AND stock >= ?", items[i].ProductID, items[i].Quantity).
UpdateColumn("stock", gorm.Expr("stock - ?", items[i].Quantity))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("库存不足")
}
if err := tx.Create(&items[i]).Error; err != nil {
return err
}
}
// 更新订单总金额
return tx.Model(order).Update("total_amt", calculateTotal(items)).Error
})
}
func calculateTotal(items []model.OrderItem) float64 {
var total float64
for _, item := range items {
total += item.Price * float64(item.Quantity)
}
return total
}
func main() {
fmt.Println("事务示例代码")
}六、软删除:GORM内置支持
只要在模型里嵌入 gorm.DeletedAt 字段,GORM自动处理软删除:
package main
import (
"fmt"
"gorm.io/gorm"
"your-project/model"
)
func softDeleteDemo(db *gorm.DB) {
// Delete:软删除,设置deleted_at,不真正删除数据
db.Delete(&model.User{}, 1)
// SQL: UPDATE users SET deleted_at=? WHERE id=1
// Find:自动过滤软删除数据(WHERE deleted_at IS NULL)
var users []model.User
db.Find(&users) // 不包含软删除的用户
// Unscoped:包含软删除的数据
db.Unscoped().Find(&users) // 包含所有用户
// 真正删除:Unscoped().Delete()
db.Unscoped().Delete(&model.User{}, 1) // 物理删除
// 恢复软删除
db.Unscoped().Model(&model.User{}).Where("id = ?", 1).
Update("deleted_at", nil)
fmt.Println("软删除示例")
}七、高级查询技巧
package main
import (
"fmt"
"gorm.io/gorm"
"your-project/model"
)
// 分页查询(带总数)
type PageResult struct {
Total int64
Data interface{}
}
func GetOrdersWithPage(db *gorm.DB, userID uint, page, pageSize int) (*PageResult, error) {
var total int64
var orders []model.Order
query := db.Model(&model.Order{}).Where("user_id = ?", userID)
// 先count(注意:count要在limit/offset之前)
if err := query.Count(&total).Error; err != nil {
return nil, err
}
offset := (page - 1) * pageSize
if err := query.
Preload("Items").
Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&orders).Error; err != nil {
return nil, err
}
return &PageResult{Total: total, Data: orders}, nil
}
// 动态条件查询
type OrderFilter struct {
UserID *uint
Status *string
MinAmount *float64
MaxAmount *float64
}
func FilterOrders(db *gorm.DB, filter OrderFilter) ([]model.Order, error) {
var orders []model.Order
query := db.Model(&model.Order{})
if filter.UserID != nil {
query = query.Where("user_id = ?", *filter.UserID)
}
if filter.Status != nil {
query = query.Where("status = ?", *filter.Status)
}
if filter.MinAmount != nil {
query = query.Where("total_amt >= ?", *filter.MinAmount)
}
if filter.MaxAmount != nil {
query = query.Where("total_amt <= ?", *filter.MaxAmount)
}
return orders, query.Find(&orders).Error
}
// 原生SQL(复杂查询用)
func GetOrderStats(db *gorm.DB, userID uint) (map[string]interface{}, error) {
var result struct {
TotalOrders int64 `gorm:"column:total_orders"`
TotalAmount float64 `gorm:"column:total_amount"`
AvgAmount float64 `gorm:"column:avg_amount"`
}
err := db.Raw(`
SELECT
COUNT(*) as total_orders,
SUM(total_amt) as total_amount,
AVG(total_amt) as avg_amount
FROM orders
WHERE user_id = ? AND deleted_at IS NULL
`, userID).Scan(&result).Error
return map[string]interface{}{
"total_orders": result.TotalOrders,
"total_amount": result.TotalAmount,
"avg_amount": result.AvgAmount,
}, err
}
func main() {
fmt.Println("高级查询示例代码")
}八、GORM vs JPA/Hibernate 对比
| 功能 | JPA/Hibernate | GORM |
|---|---|---|
| ORM映射 | 注解(@Entity等) | struct tag |
| 关联查询 | 自动懒加载(坑) | 需要Preload显式加载 |
| 事务 | @Transactional注解 | db.Transaction()或手动Begin/Commit |
| 软删除 | 手动实现 | gorm.DeletedAt内置支持 |
| 迁移 | Flyway/Liquibase | AutoMigrate(仅适合开发) |
| 原生SQL | @Query | db.Raw() |
| 分页 | Pageable | Limit+Offset |
九、总结
GORM用好的几个关键:
- 解决N+1查询:显式使用Preload或Joins预加载关联数据
- 事务用Transaction闭包:自动处理提交和回滚,代码更简洁
- 软删除用嵌入gorm.DeletedAt:一行代码实现,所有查询自动过滤
- 连接池要配置:MaxOpenConns、MaxIdleConns、ConnMaxLifetime
- 生产环境开慢查询日志:及时发现性能问题
小胡改用Preload之后,获取100条订单的接口从101次SQL变成了2次SQL(先查orders,再批量查items),响应时间从300ms降到了15ms。
N+1问题是ORM最经典的坑,理解了它,你就掌握了GORM最关键的性能优化技巧。
