3、模型定义基础:结构体标签(gorm标签)、字段类型映射、主键与自增ID、表名约定

在 GORM 中,模型(Model)是 Go 结构体与数据库表之间的映射契约。本章将深入解析结构体标签(gorm 标签)的完整语法、Go 类型与数据库类型的映射规则、主键与自增 ID 的配置方式,以及表名的默认约定与自定义策略。掌握这些基础,是构建稳定、可维护的数据库迁移体系的前提。

3.1 结构体标签:gorm 标签语法

GORM 使用结构体标签(struct tag)来声明字段的数据库属性。标签格式为 gorm:"key1:value1;key2:value2",多个键值对之间用分号分隔。以下是最常用的标签键及其含义:

标签键 说明 示例
column 指定数据库列名 gorm:"column:user_name"
type 指定数据库列类型(如 varchar(100)、text、bigint) gorm:"type:varchar(100)"
size 指定列大小(仅对某些类型有效) gorm:"size:255"
primaryKey 标记为主键 gorm:"primaryKey"
autoIncrement 启用自增(默认对整数主键生效) gorm:"autoIncrement"
unique 创建唯一约束 gorm:"unique"
not null 设置非空约束 gorm:"not null"
default 设置默认值 gorm:"default:0"
comment 添加列注释 gorm:"comment:用户昵称"
index 创建索引(可指定索引名) gorm:"index:idx_name"
uniqueIndex 创建唯一索引 gorm:"uniqueIndex:uidx_email"
embedded 嵌入结构体(展平字段) gorm:"embedded"
embeddedPrefix 嵌入字段的前缀 gorm:"embeddedPrefix:addr_"
- 忽略该字段(不参与迁移和读写) gorm:"-"

最佳实践:建议在团队中统一标签书写顺序:column → type → size → primaryKey → autoIncrement → not null → default → comment → index。保持可读性,便于代码审查。

type User struct {
    ID        uint      `gorm:"column:id;type:bigint;primaryKey;autoIncrement;not null;comment:主键ID"`
    Name      string    `gorm:"column:name;type:varchar(100);not null;default:'';comment:用户名"`
    Email     string    `gorm:"column:email;type:varchar(255);uniqueIndex;not null;comment:邮箱"`
    Age       int       `gorm:"column:age;type:int;default:0;comment:年龄"`
    CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间"`
    UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间"`
    DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:datetime;index;comment:软删除时间"`
}

3.2 字段类型映射:Go 类型 → 数据库类型

GORM 内置了一套 Go 类型到数据库类型的默认映射规则。理解这些映射,有助于在迁移时避免类型不匹配导致的错误。

Go 类型 默认数据库类型 说明
bool TINYINT(1) 布尔值映射为 0/1
int, int8, int16, int32, uint, uint8, uint16, uint32 INT 根据位数自动选择合适大小
int64, uint64 BIGINT 大整数类型
float32 FLOAT 单精度浮点
float64 DOUBLE 双精度浮点
string LONGTEXT 或 VARCHAR(255) 无 size 标签时默认 LONGTEXT;有 size 时变为 VARCHAR(size)
[]byte BLOB 二进制数据
time.Time DATETIME(3) 精确到毫秒
json.RawMessage JSON 仅 MySQL 5.7+ / PostgreSQL
gorm.DeletedAt DATETIME(3) 软删除专用

类型覆盖技巧:当默认映射不符合需求时,使用 gorm:"type:varchar(50)" 或 gorm:"type:decimal(10,2)" 显式指定数据库类型。这在金额、状态码等精确控制场景中尤为重要。

type Product struct {
    ID    uint    `gorm:"primaryKey;autoIncrement"`
    Name  string  `gorm:"type:varchar(200);not null"`
    Price float64 `gorm:"type:decimal(10,2);not null;default:0.00"`
    Stock int     `gorm:"type:int;not null;default:0"`
    Desc  string  `gorm:"type:text"`
}

3.3 主键与自增 ID

GORM 默认将名为 ID 的字段作为主键,并自动启用自增(AutoIncrement)。但实际项目中,主键策略往往需要更精细的控制。

3.3.1 默认主键行为

type User struct {
    ID   uint   // 自动识别为主键,且自增
    Name string
}

3.3.2 自定义主键字段

使用 primaryKey 标签将任意字段标记为主键:

type Order struct {
    OrderID   string `gorm:"column:order_id;type:varchar(32);primaryKey;comment:订单号"`
    UserID    uint   `gorm:"not null"`
    Amount    float64
}

3.3.3 复合主键

在结构体中标记多个 primaryKey 字段,GORM 会自动创建联合主键:

type Membership struct {
    UserID    uint   `gorm:"primaryKey;comment:用户ID"`
    GroupID   uint   `gorm:"primaryKey;comment:群组ID"`
    Role      string `gorm:"type:varchar(20);not null"`
    JoinedAt  time.Time
}

3.3.4 自增 ID 的显式控制

使用 autoIncrement 标签显式启用自增,或使用 autoIncrementIncrement 设置步长(需数据库支持):

type Article struct {
    ID      uint   `gorm:"primaryKey;autoIncrement;autoIncrementIncrement:10"`
    Title   string `gorm:"not null"`
    Content string `gorm:"type:text"`
}

注意:自增 ID 在分布式场景下存在冲突风险。对于分库分表或跨数据中心部署,建议使用雪花算法(Snowflake)或 UUID 作为主键,并关闭自增。

3.4 表名约定

GORM 默认将结构体名转换为复数形式作为表名。转换规则为:驼峰命名转下划线分隔,并自动复数化(如 User → users,OrderItem → order_items)。

3.4.1 默认表名示例

结构体名 默认表名
User users
OrderItem order_items
ProductCategory product_categories
APIRequest a_p_i_requests(连续大写字母会被拆分)

3.4.2 自定义表名:TableName 方法

为结构体实现 TableName() string 方法,可完全控制表名:

func (User) TableName() string {
    return "t_user"
}

func (OrderItem) TableName() string {
    return "order_item"  // 不使用复数
}

3.4.3 全局表名前缀

在 GORM 配置中设置 TablePrefix,可为所有表添加统一前缀:

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    NamingStrategy: schema.NamingStrategy{
        TablePrefix: "tbl_",   // 所有表名前缀
        SingularTable: false,  // 是否使用单数表名
    },
})

3.4.4 禁用复数表名

设置 SingularTable: true 后,表名与结构体名完全一致(不自动复数化):

NamingStrategy: schema.NamingStrategy{
    SingularTable: true,  // User → user, OrderItem → order_item
}

领域边界建议:在微服务架构中,建议为每个领域服务设置独立的表前缀(如 order_、user_),避免跨服务表名冲突。同时,在领域内部保持表名与实体名的一致性,降低认知负载。

3.5 模型定义完整示例

综合以上知识点,以下是一个符合领域边界设计的完整模型示例:

package model

import (
    "time"
    "gorm.io/gorm"
)

// Order 订单实体 - 订单领域
type Order struct {
    ID          uint           `gorm:"column:id;type:bigint;primaryKey;autoIncrement;not null;comment:订单ID"`
    OrderNo     string         `gorm:"column:order_no;type:varchar(32);uniqueIndex;not null;comment:订单编号"`
    UserID      uint           `gorm:"column:user_id;type:bigint;not null;index;comment:用户ID"`
    TotalAmount float64        `gorm:"column:total_amount;type:decimal(12,2);not null;default:0.00;comment:订单总金额"`
    Status      int8           `gorm:"column:status;type:tinyint;not null;default:0;comment:订单状态(0-待支付 1-已支付 2-已取消)"`
    CreatedAt   time.Time      `gorm:"column:created_at;type:datetime(3);not null;comment:创建时间"`
    UpdatedAt   time.Time      `gorm:"column:updated_at;type:datetime(3);not null;comment:更新时间"`
    DeletedAt   gorm.DeletedAt `gorm:"column:deleted_at;type:datetime(3);index;comment:删除时间"`
}

// TableName 自定义表名
func (Order) TableName() string {
    return "order_order"
}

3.6 模型定义核心流程

下图展示了从 Go 结构体到数据库表的完整映射流程,以及各环节的关键决策点:

模型定义核心映射流程 Go 结构体定义 GORM 解析结构体标签与字段类型 字段类型映射 Go 类型 → 数据库类型 主键与自增策略 primaryKey / autoIncrement 表名约定 默认复数 / TableName 方法 AutoMigrate / 迁移执行 数据库表(DDL 生成) 标签优先级 > 默认映射 > 全局配置

3.7 常见陷阱与避坑指南

  • 标签拼写错误:primaryKey 是驼峰写法,primarykey 或 primary_key 均无效,GORM 会静默忽略。
  • 自增与主键绑定:只有标记了 primaryKey 的字段才能使用 autoIncrement;非主键字段设置自增会被忽略。
  • 表名冲突:多个结构体实现相同的 TableName() 返回同一表名时,迁移会互相覆盖,导致数据丢失。
  • 类型精度丢失:Go 的 float64 映射为 DOUBLE,在金额场景下应显式指定 type:decimal(10,2) 避免精度问题。
  • 时间时区:time.Time 默认使用 UTC 存储,建议在数据库连接 DSN 中指定 loc=Local 或 loc=Asia/Shanghai。

调试技巧:在开发阶段,开启 GORM 的 Logger 日志级别为 info,可以查看 AutoMigrate 生成的 DDL 语句,便于验证标签解析是否正确。