前面的 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=Local。parseTime=True 让 time.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 注入。Count 和 Find 用同一个查询对象,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)
Author 是 Article 结构体上的 gorm:"foreignKey:AuthorID" 关联字段。不加 Preload 时关联是零值,循环里再逐个查就是经典 N+1 慢查询。
7. 接进分层
main.go 里 OpenDB 拿到 *gorm.DB,传给 router.Setup,链路和上一篇一致:
db, _ := OpenDB(cfg.DSN) r := router.Setup(db) r.Run(":" + cfg.Port)
repository 持有 db,service 持有 repo,handler 持有 service。数据库细节锁在最底层,换存储引擎只动 repo。
下一篇处理配置、优雅停机和 Docker 部署,让服务真正能上线。