GORM项目中数据库设计由结构体驱动,通过标签声明约束并用AutoMigrate建表;主键需显式标记,时间字段须配parseTime=True,AutoMigrate只增不改,分库分表需手动管理多DB实例。

数据库设计在 GORM 项目里不是先画ER图再写代码,而是由结构体定义驱动、配合标签显式声明约束,最终通过 AutoMigrate 落地为表。设计是否合理,直接反映在字段类型、主键策略、索引和关联关系的 Go 层表达上。
gorm.Model 能省事,但别无脑嵌套
内嵌 gorm.Model 自动带 ID、CreatedAt、UpdatedAt、DeletedAt,适合快速原型。但它强制主键是 uint 类型的 ID 字段,一旦你的业务主键是 string(如 UUID)、int64(如 Snowflake ID)或复合主键,就必须放弃它。
常见做法:
- 用
gorm:"primaryKey"显式标记主键字段,比如UserID string `gorm:"primaryKey"` - 若需软删除但不用
DeletedAt,可自定义字段并加gorm:"softDelete" - 时间字段统一用
time.Time,且 DSN 中必须含parseTime=True&loc=Local,否则查出来是零值
字段标签不只为了映射,更是约束声明
GORM 的 struct tag 不只是告诉“这个字段对应哪列”,它承担了建表时的 DDL 职责。漏写关键标签,AutoMigrate 就不会生成对应约束。
立即学习“go语言免费学习笔记(深入)”;
容易忽略的点:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
uniqueIndex≠unique:unique只建唯一索引,不加NOT NULL;要真正防止空值重复,得写gorm:"uniqueIndex;not null" - 联合索引靠命名对齐:
Name string `gorm:"index:idx_user_name_email"`和Email string `gorm:"index:idx_user_name_email"`才会建出复合索引 - 默认值要配可空类型才可靠:想让 MySQL 表字段有
DEFAULT 1,Go 字段得是sql.NullInt64或指针类型,并配gorm:"default:1"
AutoMigrate 不是同步工具,它只“增”不“改”
AutoMigrate 的行为非常克制:它只会新增缺失的字段、索引或表,绝不会修改已有字段类型、删列、重命名、删索引。这意味着——
你改了结构体字段类型(比如从 int 改成 int64),它不会 ALTER COLUMN;你删了一个字段,它不会 DROP COLUMN;你把 gorm:"unique" 改成 gorm:"uniqueIndex",它也不会重建索引。
所以实际开发中:
- 首次上线前务必手动确认数据库存在,
AutoMigrate不会创建 database - 字段类型变更必须搭配 SQL 手动迁移(如
ALTER TABLE ... MODIFY COLUMN) - 加一句
db.Migrator().CurrentDatabase()非空校验,能提前暴露连接失败或权限不足问题
分库分表不能靠 GORM 自动推导
GORM 官方不支持跨库分片。所谓“动态表名”只是 db.Table("users_01") 指定当前操作的表,底层仍走同一个 *gorm.DB 连接池,本质还是单库。
真要分库,必须自己管理多个 *gorm.DB 实例:
- 按分片键(如
user_id % 4)路由到对应 DB 实例 - 每个库独立调用
AutoMigrate,不能共用 - 事务无法跨库,涉及多库写入得靠应用层补偿或最终一致性
-
db.Table()只影响当次链式调用,Preload 关联查询、事务范围都不会自动继承该表名
最易被忽略的是:分表逻辑必须写在模型操作之前,GORM 不会根据结构体字段值自动拼表名——你得自己算后缀、自己传进去。

















