← 返回博客
go2026-09-12 20:30:322 分钟 · 588 0

Gin 接入 MySQL:用 GORM 做 CRUD、事务与迁移

用 GORM 连接 MySQL,配置连接池,做 AutoMigrate、CRUD、事务和预加载,并接进第 5 篇的分层结构。

#mysql#gin#GORM

前面的 service 还是假数据,这一篇接上真实的 MySQL。GORM 是 Go 里最主流的 ORM,把结构体映射成表,少写 SQL。重点讲连接池、事务和迁移这三块容易出事的地方。

1. 连接与连接池

import (
    "gorm.io/gorm"
    "gorm.io/driver/mysql"
)

func OpenDB(dsn string) (*gorm.DB, error) {
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, err
    }
    sqlDB, _ := db.DB()
    sqlDB.SetMaxOpenConns(50)  // 最大打开连接
    sqlDB.SetMaxIdleConns(10)  // 最大空闲连接
    sqlDB.SetConnMaxLifetime(time.Hour) // 连接最长存活,避免用到被服务端关闭的脏连接
    return db, nil
}

DSN 格式:user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=LocalparseTime=Truetime.Time 能直接映射 datetime,loc=Local 防止时区错位。

连接池必须配。不配时默认无上限,突发流量会打满 MySQL 连接数,报 too many connections

2. 定义模型与迁移

模型用结构体描述表,GORM 的标签控制列名和约束:

type User struct {
    ID        uint      `gorm:"primaryKey" json:"id"`
    Username  string    `gorm:"size:50;uniqueIndex" json:"username"`
    Password  string    `gorm:"size:100" json:"-"`
    Status    int       `gorm:"default:1" json:"status"`
    CreatedAt time.Time `json:"created_at"`
}

json:"-" 让密码永远不进响应。启动时自动建表:

db.AutoMigrate(&User{}, &Article{})

AutoMigrate 只增不减:加字段会 ALTER,删字段不会自动删列。生产环境改表结构前先备份,大表 ALTER 会锁表。

3. 基础 CRUD

放到 repository 层,handler 不碰 gorm.DB

func (r *UserRepo) Create(u *User) error {
    return r.db.Create(u).Error
}

func (r *UserRepo) FindByID(id uint) (*User, error) {
    var u User
    err := r.db.First(&u, id).Error
    return &u, err
}

func (r *UserRepo) UpdateStatus(id uint, status int) error {
    return r.db.Model(&User{}).Where("id = ?", id).
        Update("status", status).Error
}

func (r *UserRepo) Delete(id uint) error {
    return r.db.Delete(&User{}, id).Error
}

查询没找到时 GORM 返回 gorm.ErrRecordNotFound,service 层把它转成 CodeNotFound 的业务错误。

4. 条件查询与分页

列表接口用 Where 拼条件,再算总数和分页:

func (r *ArticleRepo) List(keyword string, page, size int) ([]Article, int64, error) {
    var list []Article
    var total int64

    q := r.db.Model(&Article{})
    if keyword != "" {
        q = q.Where("title LIKE ?", "%"+keyword+"%")
    }
    q.Count(&total)
    q.Offset((page - 1) * size).Limit(size).Order("id DESC").Find(&list)
    return list, total, nil
}

LIKE 拼接用参数占位 %?%,别直接字符串拼接,避免 SQL 注入。CountFind 用同一个查询对象,GORM 会保留 Where 条件。

5. 事务

涉及多表写入要包事务,要么全成要么全回滚。GORM 用 Transaction 自动提交和回滚:

func (s *OrderService) Create(order *Order, items []OrderItem) error {
    return s.db.Transaction(func(tx *gorm.DB) error {
        if err := tx.Create(order).Error; err != nil {
            return err // 返回错误自动回滚
        }
        for i := range items {
            items[i].OrderID = order.ID
            if err := tx.Create(&items[i]).Error; err != nil {
                return err
            }
        }
        return nil // 全成功才提交
    })
}

回调里 return err 即触发回滚,return nil 才提交。千万别在事务里起 goroutine 做写库,事务绑定在调用方的连接上,协程里拿不到同一事务。

6. 关联预加载

查文章带作者,用 Preload 一次性把关联拉出来,避免 N+1 查询:

var articles []Article
db.Preload("Author").Find(&articles)

AuthorArticle 结构体上的 gorm:"foreignKey:AuthorID" 关联字段。不加 Preload 时关联是零值,循环里再逐个查就是经典 N+1 慢查询。

7. 接进分层

main.goOpenDB 拿到 *gorm.DB,传给 router.Setup,链路和上一篇一致:

db, _ := OpenDB(cfg.DSN)
r := router.Setup(db)
r.Run(":" + cfg.Port)

repository 持有 db,service 持有 repo,handler 持有 service。数据库细节锁在最底层,换存储引擎只动 repo。

下一篇处理配置、优雅停机和 Docker 部署,让服务真正能上线。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/183