第 7 章 数据模型与迁移
本文导航

第 7 章 数据模型与迁移
Owl 用 GORM 管理数据库,并在启动时统一执行 AutoMigrate。这一章说明模型怎么写、迁移什么时候执行、种子数据放哪里。
基础模型
大多数业务表嵌入 db.BaseModel:
type BaseModel struct {
ID uint `gorm:"primarykey" json:"id,string"`
CreatedAt *time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *gorm.DeletedAt `gorm:"index" json:"deletedAt,omitempty"`
CreatorID string `gorm:"comment:创建人" json:"creatorID,omitempty"`
CreatorName string `gorm:"comment:创建人名称" json:"creatorName,omitempty"`
UpdaterID string `gorm:"comment:修改人" json:"updaterID,omitempty"`
UpdaterName string `gorm:"comment:修改人名称" json:"updaterName,omitempty"`
}
它提供:
- 雪花 ID:
BeforeCreate自动生成,手动设置了 ID 则不覆盖; - 创建/更新时间与软删除;
- 从请求上下文自动写入创建人、修改人(键为
user_id、username)。
ID 的 JSON 标签是 id,string,避免前端 JS 处理雪花 ID 时丢精度。
写一个模型
package model
import (
"bit-labs.cn/owl/provider/db"
"gorm.io/gorm/schema"
)
const (
OrderStatusPending = 1 // 待支付
OrderStatusPaid = 2 // 已支付
OrderStatusCancelled = 3 // 已取消
)
var _ schema.Tabler = (*Order)(nil)
type Order struct {
db.BaseModel
OrderNo string `gorm:"comment:订单号;size:64;uniqueIndex" json:"orderNo"`
Title string `gorm:"comment:标题;size:255" json:"title"`
Amount int64 `gorm:"comment:金额,分" json:"amount"`
Status int `gorm:"comment:状态" json:"status"`
}
func (i *Order) TableName() string { return "order_order" }
约定:
- 状态、类型、来源等有业务含义的值必须定义具名常量,禁止裸数字(
OrderStatusPaid,不要Status: 2); - 枚举值不要用 0,从 1 开始;
- 模型保持数据库无关,不要写死 MySQL/PostgreSQL 专有类型;
- 是否软删除取决于是否嵌入
BaseModel,不需要软删除时可以自定义主键与时间字段。
迁移怎么执行
子应用实现:
func (i *SubAppOrder) RegisterMigrate() []any {
return []any{
&model.Order{},
&model.OrderItem{},
}
}
启动时框架:
- 执行所有子应用的
BeforeMigrate(串行); - 收集所有
RegisterMigrate()的模型; - 按类型去重,计算 schema hash(并行);
- 只对 hash 变化的模型执行
AutoMigrate(串行); - 把 hash 写入
storage/migrate_hash.txt; - 执行所有子应用的
AfterMigrate(并行)。
hash 文件示例:
bit-labs.cn/my-order/app/model.Order=8f14e45fceea167a
bit-labs.cn/my-order/app/model.OrderItem=2c9a1b0e3d4f5a6b
注意:
- 结构没变就不会重复迁移;
BeforeMigrate/AfterMigrate每次启动都执行;- 想强制重迁某个模型,删掉 hash 文件里对应行;
- 不要在
Bootstrap()里手动AutoMigrate,否则会绕过 hash 门控。
种子数据放哪里
幂等种子、修补逻辑放 AfterMigrate:
func (i *SubAppOrder) AfterMigrate(gdb *gorm.DB) error {
return seeder.InitOrderStatus(gdb)
}
func InitOrderStatus(gdb *gorm.DB) error {
var count int64
gdb.Model(&model.Order{}).Where("code = ?", "DEFAULT").Count(&count)
if count > 0 {
return nil
}
return gdb.Create(&model.Order{OrderNo: "DEFAULT", Title: "默认订单"}).Error
}
如果种子可以延迟执行,可以在 AfterMigrate 内自行 go,框架不会替你异步化。
Repository:复用泛型基类
package repository
import (
"context"
"bit-labs.cn/my-order/app/model"
"bit-labs.cn/owl/provider/db"
"gorm.io/gorm"
)
type OrderRepositoryInterface interface {
FindById(id any) (*model.Order, error)
Save(o *model.Order) error
Retrieve(page, pageSize int, fn func(db *gorm.DB)) (int64, []model.Order, error)
WithContext(ctx context.Context) OrderRepositoryInterface
}
type OrderRepository struct {
db.BaseRepository[model.Order]
ctx context.Context
}
func NewOrderRepository(gdb *gorm.DB) *OrderRepository {
return &OrderRepository{BaseRepository: db.NewBaseRepository[model.Order](gdb)}
}
func (i *OrderRepository) WithContext(ctx context.Context) OrderRepositoryInterface {
i.BaseRepository.WithContext(ctx)
i.ctx = ctx
return i
}
func (i *OrderRepository) FindById(id any) (*model.Order, error) {
return i.Detail(id)
}
func (i *OrderRepository) Save(o *model.Order) error {
return i.BaseRepository.Save(o)
}
func (i *OrderRepository) Retrieve(page, pageSize int, fn func(db *gorm.DB)) (int64, []model.Order, error) {
return i.BaseRepository.Retrieve(page, pageSize, fn)
}
BaseRepository 已提供 Save、Delete、Detail、Retrieve、Unique、ChangeStatus。仓储层只写业务特有的查询,通用的增删改查直接复用。
分页查询的常见写法
count, list, err := repo.WithContext(ctx).Retrieve(req.Page, req.PageSize, func(tx *gorm.DB) {
db.AppendWhereFromStruct(tx, req) // 按结构体字段拼条件
tx.Order("created_at desc")
})
AppendWhereFromStruct 会读取结构体中的非零字段,把字段名转成下划线列名拼条件;字段名以 Like、Gt、Gte、Lt、Lte、In 结尾时会转成对应的比较操作。分页参数用 router.PageReq,会被自动跳过。
自检
- [ ] 模型实现了
TableName(); - [ ] 状态常量与模型同包定义,且从 1 开始;
- [ ] 新模型已加入
RegisterMigrate(); - [ ] 没有在
Bootstrap()里手动迁移; - [ ] 种子逻辑幂等;
- [ ] 仓储接口有
WithContext,查询带上上下文。
评论
加载评论中…