第 4 章 · GORM 与 MySQL 建模
本章目标:使用 GORM 定义 Product、User 模型;执行 AutoMigrate 自动建表;字段与 django-web、fastapi-web、flask-web 对齐:slug + is_published,价格 int64 分;User 含 email、password_hash(bcrypt)、role;可选 docker compose 启动 MySQL 8;对照 fastapi-web ch04/ch05 SQLAlchemy 模型。
学时建议:4~5 小时(含 2 小时跟练)
前置:完成 gin-web ch03;基本 SQL;Docker 可选。系统 SQL/库表设计见 go-database ch01~ch07;database/sql 与 GORM 深入见 go-database ch08~ch12。
4.1 场景说明:持久化商品与用户
ch02 的 mock 数据需迁入 MySQL。api-go-demo 与全站课程统一商品字段,禁止使用旧字段 is_active 或 float 元价格。
| 实体 | 字段 | 类型 | 说明 |
|---|---|---|---|
| Product | slug | varchar(64) unique | URL 标识 |
| Product | name | varchar(200) | 名称 |
| Product | price | bigint | 分,如 6800 = 68.00 元 |
| Product | stock | int | 库存 |
| Product | is_published | bool | 上架状态 |
| Product | description | text | 可选描述 |
| User | email | varchar(255) unique | 登录名 |
| User | password_hash | varchar(255) | bcrypt,非明文 |
| User | role | varchar(32) | user / admin |
internal/
├── model/
│ ├── product.go
│ └── user.go
└── database/
├── mysql.go
└── migrate.go
对照章节:
| 主题 | flask-web ch05 | fastapi-web ch05 | gin-web ch04 |
|---|---|---|---|
| ORM | SQLAlchemy | SQLAlchemy 2.0 | GORM |
| 迁移 | Flask-Migrate | Alembic | AutoMigrate / migrate |
| 价格 | 分或 Decimal | Decimal 元 | int64 分 |
4.2 逐步操作表
| 步骤 | 操作 | 验证 |
|---|---|---|
| 1 | go get gorm.io/gorm gorm.io/driver/mysql | go.mod 更新 |
| 2 | docker compose up -d mysql(选修) | 3306 可连 |
| 3 | 配置 DATABASE_URL | config.Load 通过 |
| 4 | 编写 Product/User model | 编译通过 |
| 5 | AutoMigrate | 表 products、users 存在 |
| 6 | SeedProducts | 3 条种子数据 |
| 7 | 查询 is_published=true | 返回 2 条 |
| 8 | 重复 slug 插入 | 唯一约束报错 |
4.3 Docker Compose(选修)
项目根 docker-compose.yml:
services:
mysql:
image: mysql:8.0
container_name: api-go-demo-mysql
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: api_demo
MYSQL_USER: api_demo
MYSQL_PASSWORD: api_demo_pass
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
volumes:
mysql_data:
docker compose up -d
docker compose ps
.env 中 DSN:
DATABASE_URL=api_demo:api_demo_pass@tcp(127.0.0.1:3306)/api_demo?charset=utf8mb4&parseTime=True&loc=Local
| 参数 | 作用 |
|---|---|
charset=utf8mb4 | 支持 emoji 与中文 |
parseTime=True | time.Time 正确扫描 |
loc=Local | 本地时区 |
4.4 依赖与 MySQL 连接
go get gorm.io/gorm@v1.25.12
go get gorm.io/driver/mysql@v1.5.7
// internal/database/mysql.go
package database
import (
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func NewMySQL(dsn string, debug bool) (*gorm.DB, error) {
lvl := logger.Warn
if debug {
lvl = logger.Info
}
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(lvl),
NowFunc: func() time.Time {
return time.Now().Local()
},
})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(time.Hour)
return db, nil
}
DSN 来自 ch03 DATABASE_URL;GORM mysql driver 接受 user:pass@tcp(host:port)/db?params 格式。
4.5 Product 模型(完整)
// internal/model/product.go
package model
import "time"
type Product struct {
ID uint `gorm:"primaryKey"`
Slug string `gorm:"uniqueIndex;size:64;not null"`
Name string `gorm:"size:200;not null"`
Price int64 `gorm:"not null"` // 单位:分
Stock int `gorm:"default:0;not null"`
IsPublished bool `gorm:"column:is_published;default:false;not null"`
Description string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (Product) TableName() string { return "products" }
// PriceYuan 仅展示用,禁止写入 DB
func (p Product) PriceYuan() float64 {
return float64(p.Price) / 100.0
}
| GORM tag | 数据库效果 |
|---|---|
uniqueIndex | UNIQUE KEY on slug |
column:is_published | 列名 snake_case |
size:64 | VARCHAR(64) |
4.6 User 模型(完整)
// internal/model/user.go
package model
import "time"
type User struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex;size:255;not null"`
PasswordHash string `gorm:"column:password_hash;size:255;not null"`
Role string `gorm:"size:32;default:user;not null"` // user | admin
IsActive bool `gorm:"column:is_active;default:true;not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (User) TableName() string { return "users" }
| 角色 | 权限预告(ch06 RBAC) |
|---|---|
user | 读已发布商品、改自己资料 |
admin | 商品 CRUD、上下架 |
注意:密码存 password_hash,由 ch06 bcrypt.GenerateFromPassword 写入,模型层不含明文密码字段。