第05章:模型定义基础 —— 结构体到表的映射
本章目标
学完本章后,你将能够:
- 理解 GORM 模型的定义方式(结构体 + 标签)
- 使用
gorm.Model快速获得标准字段 - 理解"约定优于配置"在表名和列名中的体现
- 用
AutoMigrate()自动创建数据库表 - 独立定义一个完整的 GORM 模型并成功映射到数据库
前置知识
- 需要先学习:第04章(GORM 配置与 Logger)
- 需要了解:MySQL 的 CREATE TABLE 语句、数据类型、主键概念
- 需要准备:上一章配置好的 GORM 连接
5.1 什么是 GORM 模型
在 GORM 中,模型(Model)= 一个 Go 结构体。
你把结构体定义好,GORM 就能自动在数据库中创建对应的表:
Go 结构体 数据库表
┌─────────────────────┐ ┌──────────────────────┐
│ type User struct { │ │ CREATE TABLE users ( │
│ ID uint │ ───→ │ id BIGINT PRIMARY, │
│ Name string │ │ name VARCHAR(255), │
│ Age int │ │ age INT, │
│ } │ │ ) │
└─────────────────────┘ └──────────────────────┘5.2 第一个模型定义
让我们从最简单的例子开始。创建一个新文件 model.go:
package main
import (
"gorm.io/gorm"
)
// User 用户模型 → 对应数据库中的 users 表
type User struct {
ID uint // 主键
Name string // 用户名
Age int // 年龄
Email string // 邮箱
}这 5 行代码定义了一个 GORM 模型。没有任何标签(tag),GORM 会按默认约定来处理:
| 结构体 | 默认行为 |
|---|---|
User | 表名:users(复数、蛇形) |
ID | 列名:id,类型:BIGINT,主键,自增 |
Name | 列名:name,类型:VARCHAR(255) |
Age | 列名:age,类型:BIGINT |
Email | 列名:email,类型:VARCHAR(255) |
用 AutoMigrate 创建表
package main
import (
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type User struct {
ID uint
Name string
Age int
Email string
}
func main() {
dsn := "root:123456@tcp(127.0.0.1:3306)/gorm_tutorial?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
log.Fatal(err)
}
// AutoMigrate:自动创建表!
err = db.AutoMigrate(&User{})
if err != nil {
log.Fatal(err)
}
log.Println("✅ 表结构创建成功!")
}运行后,GORM 的日志会输出:
CREATE TABLE `users` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255),
`age` BIGINT,
`email` VARCHAR(255),
PRIMARY KEY (`id`)
)在 MySQL 中验证:
USE gorm_tutorial;
DESC users;
-- +-------+-----------------+------+-----+---------+----------------+
-- | Field | Type | Null | Key | Default | Extra |
-- +-------+-----------------+------+-----+---------+----------------+
-- | id | bigint unsigned | NO | PRI | NULL | auto_increment |
-- | name | varchar(255) | YES | | NULL | |
-- | age | bigint | YES | | NULL | |
-- | email | varchar(255) | YES | | NULL | |
-- +-------+-----------------+------+-----+---------+----------------+5.3 gorm.Model:标准模型基础
GORM 提供了一个预定义的 gorm.Model 结构体,包含了四个最常用的字段:
// GORM 源码中的 gorm.Model 定义
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}在你的模型中使用
import "gorm.io/gorm"
type User struct {
gorm.Model // 内嵌!自动获得 ID, CreatedAt, UpdatedAt, DeletedAt
Name string
Age int
Email string
}嵌入 gorm.Model 后,你的 User 结构体实际包含的字段:
| 字段 | 类型 | 作用 |
|---|---|---|
ID | uint | 主键,自增 |
CreatedAt | time.Time | 创建时间,创建时自动填入当前时间 |
UpdatedAt | time.Time | 更新时间,更新时自动填入当前时间 |
DeletedAt | gorm.DeletedAt | 软删除时间,非空表示"已删除" |
// 使用 gorm.Model 后生成的表结构
// CREATE TABLE `users` (
// `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
// `created_at` DATETIME(3),
// `updated_at` DATETIME(3),
// `deleted_at` DATETIME(3), -- 索引列,软删除标记
// `name` VARCHAR(255),
// `age` BIGINT,
// `email` VARCHAR(255)
// )何时使用 gorm.Model
| 场景 | 建议 |
|---|---|
| 大部分业务表 | ✅ 使用 gorm.Model,省事 |
| 中间表(多对多关联表) | ❌ 不需要 |
| 不需要软删除 | 手动定义 ID、CreatedAt、UpdatedAt |
| 主键不是 uint | 手动定义,不用 gorm.Model |
| 主键是 UUID(string) | 手动定义 |
📌 推荐:除非有特殊需求,否则一律嵌入
gorm.Model。四个字段看起来多,但它们是数据库表的"标配"——几乎所有表都需要主键、创建时间、更新时间和软删除支持。
5.4 约定优于配置(Convention over Configuration)
GORM 的核心理念是:你遵循约定,GORM 自动处理;你要自定义,GORM 也支持。 理解这套约定是学好 GORM 的基础。
约定1:主键约定
| 规则 | 示例 |
|---|---|
名为 ID 的字段默认是主键 | ID uint → 主键 |
类型为 uint 或 int 时默认自增 | ID uint → AUTO_INCREMENT |
标签 primaryKey 可指定任意字段为主键 | UserID string \gorm:"primaryKey"`` |
约定2:表名约定
| 规则 | 示例 |
|---|---|
| 结构体名 → 复数蛇形 = 表名 | User → users |
ProductCategory → product_categories | |
APIUser → api_users |
约定3:列名约定
| 规则 | 示例 |
|---|---|
| 字段名 → 蛇形 = 列名 | UserName → user_name |
CreatedAt → created_at | |
ID → id |
约定4:时间约定
| 规则 | 示例 |
|---|---|
字段名为 CreatedAt | 创建时自动设置为当前时间 |
字段名为 UpdatedAt | 更新时自动设置为当前时间 |
字段名为 DeletedAt(gorm.DeletedAt 类型) | 软删除标记 |
自定义 vs 约定
每一条约定都可以被覆盖:
type User struct {
// 自定义主键
UserID string `gorm:"primaryKey;column:user_id;type:varchar(36)"`
// 自定义列名
UserName string `gorm:"column:username"`
}
// 自定义表名
func (User) TableName() string {
return "sys_users"
}5.5 结构体标签语法详解
结构体标签是 GORM 最强大的配置手段。格式如下:
type User struct {
// 标签放在字段类型后面,用反引号包裹
FieldName FieldType `gorm:"选项1:值1;选项2:值2;选项3"`
// 多个标签用空格分隔
FieldName FieldType `gorm:"column:xxx" json:"xxx"`
}标签的基本规则
type User struct {
// ✅ 正确的标签写法
Name string `gorm:"column:user_name;type:varchar(100);not null;default:'';comment:用户名"`
// ✅ 标签太长可以这样写(实际还是同一行)
Email string `gorm:"column:email;type:varchar(255);uniqueIndex;not null"`
// ✅ 不需要标签时可以不写
Age int
// ✅ 多个标签(GORM 标签 + JSON 标签 + 其他)
Name string `gorm:"column:name" json:"name" validate:"required"`
// ❌ 错误:反引号内包含反引号
// Field string `gorm:"column:`name`"` // 这不行!
// ❌ 错误:标签没闭合
// Field string `gorm:"column:name` // 少了后反引号
}📌 GORM 标签的格式:
`gorm:"key1:value1;key2:value2;...;flag"`。选项之间用分号分隔,值是可选的(not null、primaryKey这类 flag 不需要值)。
5.6 完整模型示例
来看一个完整的、带标签的模型定义:
package models
import (
"time"
"gorm.io/gorm"
)
// User 用户模型
// 对应数据库中的 users 表
type User struct {
gorm.Model // ID, CreatedAt, UpdatedAt, DeletedAt
Name string `gorm:"column:name;type:varchar(100);not null;comment:用户名"`
Age int `gorm:"column:age;type:int;default:18;comment:年龄"`
Email string `gorm:"column:email;type:varchar(255);uniqueIndex;not null;comment:邮箱"`
Phone string `gorm:"column:phone;type:varchar(20);index;comment:手机号"`
Birthday *time.Time `gorm:"column:birthday;type:date;comment:生日"`
IsActive bool `gorm:"column:is_active;type:tinyint(1);default:1;comment:是否激活"`
}
// TableName 自定义表名
func (User) TableName() string {
return "users" // 明确指定表名(推荐做法)
}执行 db.AutoMigrate(&User{}) 后生成的 SQL:
CREATE TABLE `users` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`created_at` DATETIME(3),
`updated_at` DATETIME(3),
`deleted_at` DATETIME(3),
`name` VARCHAR(100) NOT NULL COMMENT '用户名',
`age` INT DEFAULT 18 COMMENT '年龄',
`email` VARCHAR(255) NOT NULL COMMENT '邮箱',
`phone` VARCHAR(20) COMMENT '手机号',
`birthday` DATE COMMENT '生日',
`is_active` TINYINT(1) DEFAULT 1 COMMENT '是否激活',
PRIMARY KEY (`id`),
UNIQUE INDEX `idx_users_email` (`email`),
INDEX `idx_users_deleted_at` (`deleted_at`),
INDEX `idx_users_phone` (`phone`)
)字段详解
| 字段 | Go 类型 | 标签解析 | 数据库效果 |
|---|---|---|---|
ID | uint | 来自 gorm.Model,自动 primaryKey | id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY |
CreatedAt | time.Time | 来自 gorm.Model | created_at DATETIME(3) |
UpdatedAt | time.Time | 来自 gorm.Model | updated_at DATETIME(3) |
DeletedAt | gorm.DeletedAt | 来自 gorm.Model,自动 index | deleted_at DATETIME(3) + 索引 |
Name | string | not null | name VARCHAR(100) NOT NULL |
Age | int | default:18 | age INT DEFAULT 18 |
Email | string | uniqueIndex;not null | email VARCHAR(255) NOT NULL + 唯一索引 |
Phone | string | index | phone VARCHAR(20) + 普通索引 |
Birthday | *time.Time | type:date | birthday DATE,用指针支持 NULL |
IsActive | bool | default:1 | is_active TINYINT(1) DEFAULT 1 |
常见错误
错误1:小写字段被忽略
type User struct {
Name string // ✅ 可导出,GORM 会映射
email string // ❌ 不可导出,GORM 完全忽略!
}原因:Go 语言中,首字母小写的字段对外不可见(包括对 GORM 不可见)。
解决:所有需要映射到数据库的字段,首字母必须大写。
错误2:以为 AutoMigrate 能修改列名
// 第一次定义
type User struct {
UserName string // → 列名 user_name
}
// 后改为
type User struct {
UserFullName string // → 列名 user_full_name(新增列)
// ❌ user_name 列还在!AutoMigrate 不会删除旧列
}原因:AutoMigrate 不会删除列,只会新增列。这是出于数据安全考虑。
解决:要么手动用 Migrator().DropColumn() 删除旧列,要么一开始就想好字段名。
错误3:忘记了 not null 标签导致全表可空
type User struct {
Name string // 默认允许 NULL
Age int // 默认允许 NULL
}这在业务上可能不合理——用户名和年龄通常不应该为空。
// ✅ 加上约束
type User struct {
Name string `gorm:"not null"`
Age int `gorm:"not null"`
}错误4:bool 类型存储的困惑
type User struct {
IsActive bool // → TINYINT(1),true=1, false=0
}MySQL 没有真正的 BOOLEAN,GORM 用 TINYINT(1) 代替。true 存为 1,false 存为 0。
本章小结
- 模型 = Go 结构体,模型实例 = 表的一行数据
gorm.Model提供标准字段:ID、CreatedAt、UpdatedAt、DeletedAt- GORM 约定:表名复数蛇形、列名蛇形、ID = 主键
- 所有约定都可以通过标签或方法覆盖
AutoMigrate()自动建表,但不会删除已存在的列- 字段首字母必须大写才能被 GORM 识别
练习题
- 定义一个
Product模型,包含字段:ID、商品名(Name)、价格(Price,float64)、库存(Stock,int)、创建时间、更新时间。使用gorm.Model。 - 在上一题的基础上,给
Product模型添加合适的标签:Name 不能为空且唯一、Price 类型为decimal(10,2)、Stock 默认值为 0。 - 写一个完整的 Go 程序:定义模型 → 连接数据库 → AutoMigrate → 验证表结构。
gorm.Model包含哪四个字段?分别有什么作用?- GORM 默认将
UserDetail结构体映射为什么表名?为什么? - (思考题)如果一个字段名是
IPAddress,GORM 默认会映射为什么列名?如果你希望映射为ip_address,需要怎么做?