Chapter 07

数据库集成

用 GORM 连接 PostgreSQL/MySQL,通过依赖注入把 *gorm.DB 传入各层,实现 CRUD API 与安全的事务处理。

7.1 数据库相关名词

database/sql
Go 标准库的数据库抽象层,定义 DB/Rows/Tx 接口,通过 driver 连接具体数据库。
GORM
Go 最流行的 ORM,提供模型映射、链式查询、关联、迁移、钩子;底层仍走 database/sql。
连接池(Connection Pool)
*sql.DB 内部维护的一组复用连接,用 MaxOpenConns/MaxIdleConns/ConnMaxLifetime 调优。
依赖注入(Dependency Injection)
*gorm.DB 等依赖显式传入 repository/service,而非全局变量,利于测试与解耦。
Repository(仓储层)
封装数据访问细节的一层,对上暴露领域方法(如 Create/FindByID),对下操作 GORM。
事务(Transaction)
一组要么全成功要么全回滚的操作,GORM 用 db.Transaction(fn) 或手动 Begin/Commit/Rollback。
迁移(Migration)
把模型结构同步到数据库表,GORM 提供 AutoMigrate,生产建议用独立迁移工具管理。

7.2 连接 GORM

// internal/db/db.go
import (
    "time"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

func New(dsn string) (*gorm.DB, error) {
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Warn),
    })
    if err != nil {
        return nil, err
    }
    // 连接池调优
    sqlDB, _ := db.DB()
    sqlDB.SetMaxOpenConns(50)                  // 最大打开连接
    sqlDB.SetMaxIdleConns(10)                  // 最大空闲连接
    sqlDB.SetConnMaxLifetime(time.Hour)       // 连接最长存活
    return db, nil
}
连接池要匹配数据库上限

PostgreSQL 默认 max_connections=100。若 4 个服务实例各开 50 连接就爆了。规划公式约为:单实例 MaxOpenConns × 实例数 ≤ DB 上限 × 0.8,留余量给运维连接。

7.3 定义模型

type User struct {
    ID        uint      `gorm:"primaryKey" json:"id"`
    Name      string    `gorm:"size:64;not null" json:"name"`
    Email     string    `gorm:"uniqueIndex;size:128" json:"email"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// 开发期自动建表(生产用迁移工具)
db.AutoMigrate(&User{})

7.4 依赖注入:Repository 模式

// internal/repository/user.go
type UserRepo struct { db *gorm.DB }

func NewUserRepo(db *gorm.DB) *UserRepo { return &UserRepo{db: db} }

func (r *UserRepo) Create(ctx context.Context, u *User) error {
    return r.db.WithContext(ctx).Create(u).Error
}

func (r *UserRepo) FindByID(ctx context.Context, id uint) (*User, error) {
    var u User
    err := r.db.WithContext(ctx).First(&u, id).Error
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, errcode.ErrUserNotFound
    }
    return &u, err
}

func (r *UserRepo) List(ctx context.Context, offset, limit int) ([]User, int64, error) {
    var users []User
    var total int64
    q := r.db.WithContext(ctx).Model(&User{})
    q.Count(&total)
    err := q.Offset(offset).Limit(limit).Find(&users).Error
    return users, total, err
}
始终传 context

WithContext(ctx) 把 Gin 请求的 c.Request.Context() 一路带下去,客户端断开或超时能自动取消 SQL 查询,避免慢请求堆积拖垮数据库。

7.5 装配到 Gin

func main() {
    db, err := dbpkg.New(os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err)
    }
    userRepo := repository.NewUserRepo(db)
    userSvc := service.NewUserService(userRepo)
    userHdl := handler.NewUserHandler(userSvc)

    r := gin.Default()
    api := r.Group("/api/v1")
    {
        api.POST("/users", userHdl.Create)
        api.GET("/users/:id", userHdl.Get)
        api.GET("/users", userHdl.List)
    }
    r.Run(":8080")
}

7.6 事务处理

// 转账:扣款与入账必须同成功同失败
func (r *AccountRepo) Transfer(ctx context.Context, from, to uint, amount int64) error {
    return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        // 行级锁,防并发超扣
        var acc Account
        if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
            First(&acc, from).Error; err != nil {
            return err
        }
        if acc.Balance < amount {
            return errors.New("余额不足") // 返回 error 即自动回滚
        }
        if err := tx.Model(&Account{}).Where("id = ?", from).
            Update("balance", gorm.Expr("balance - ?", amount)).Error; err != nil {
            return err
        }
        return tx.Model(&Account{}).Where("id = ?", to).
            Update("balance", gorm.Expr("balance + ?", amount)).Error
    }) // fn 返回 nil 自动 Commit,返回 error 自动 Rollback
}
事务中别用外层 db

事务闭包里所有操作必须用参数 tx,误用外层 r.db 会走池里另一条连接,不在事务内,导致部分提交、数据不一致。这是新手最常见的坑。

7.7 小结