Skip to content

第05章:模型定义基础 —— 结构体到表的映射 ​

本章目标 ​

学完本章后,你将能够:

  1. 理解 GORM 模型的定义方式(结构体 + 标签)
  2. 使用 gorm.Model 快速获得标准字段
  3. 理解"约定优于配置"在表名和列名中的体现
  4. 用 AutoMigrate() 自动创建数据库表
  5. 独立定义一个完整的 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:

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 创建表 ​

go
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 的日志会输出:

sql
CREATE TABLE `users` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(255),
    `age` BIGINT,
    `email` VARCHAR(255),
    PRIMARY KEY (`id`)
)

在 MySQL 中验证:

sql
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 结构体,包含了四个最常用的字段:

go
// GORM 源码中的 gorm.Model 定义
type Model struct {
	ID        uint           `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
}

在你的模型中使用 ​

go
import "gorm.io/gorm"

type User struct {
	gorm.Model        // 内嵌!自动获得 ID, CreatedAt, UpdatedAt, DeletedAt
	Name       string
	Age        int
	Email      string
}

嵌入 gorm.Model 后,你的 User 结构体实际包含的字段:

字段类型作用
IDuint主键,自增
CreatedAttime.Time创建时间,创建时自动填入当前时间
UpdatedAttime.Time更新时间,更新时自动填入当前时间
DeletedAtgorm.DeletedAt软删除时间,非空表示"已删除"
go
// 使用 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 约定 ​

每一条约定都可以被覆盖:

go
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 最强大的配置手段。格式如下:

go
type User struct {
	// 标签放在字段类型后面,用反引号包裹
	FieldName FieldType `gorm:"选项1:值1;选项2:值2;选项3"`
	
	// 多个标签用空格分隔
	FieldName FieldType `gorm:"column:xxx" json:"xxx"`
}

标签的基本规则 ​

go
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 完整模型示例 ​

来看一个完整的、带标签的模型定义:

go
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:

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 类型标签解析数据库效果
IDuint来自 gorm.Model,自动 primaryKeyid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
CreatedAttime.Time来自 gorm.Modelcreated_at DATETIME(3)
UpdatedAttime.Time来自 gorm.Modelupdated_at DATETIME(3)
DeletedAtgorm.DeletedAt来自 gorm.Model,自动 indexdeleted_at DATETIME(3) + 索引
Namestringnot nullname VARCHAR(100) NOT NULL
Ageintdefault:18age INT DEFAULT 18
EmailstringuniqueIndex;not nullemail VARCHAR(255) NOT NULL + 唯一索引
Phonestringindexphone VARCHAR(20) + 普通索引
Birthday*time.Timetype:datebirthday DATE,用指针支持 NULL
IsActivebooldefault:1is_active TINYINT(1) DEFAULT 1

常见错误 ​

错误1:小写字段被忽略 ​

go
type User struct {
	Name  string  // ✅ 可导出,GORM 会映射
	email string  // ❌ 不可导出,GORM 完全忽略!
}

原因:Go 语言中,首字母小写的字段对外不可见(包括对 GORM 不可见)。

解决:所有需要映射到数据库的字段,首字母必须大写。

错误2:以为 AutoMigrate 能修改列名 ​

go
// 第一次定义
type User struct {
	UserName string  // → 列名 user_name
}

// 后改为
type User struct {
	UserFullName string  // → 列名 user_full_name(新增列)
	// ❌ user_name 列还在!AutoMigrate 不会删除旧列
}

原因:AutoMigrate 不会删除列,只会新增列。这是出于数据安全考虑。

解决:要么手动用 Migrator().DropColumn() 删除旧列,要么一开始就想好字段名。

错误3:忘记了 not null 标签导致全表可空 ​

go
type User struct {
	Name string  // 默认允许 NULL
	Age  int     // 默认允许 NULL
}

这在业务上可能不合理——用户名和年龄通常不应该为空。

go
// ✅ 加上约束
type User struct {
	Name string `gorm:"not null"`
	Age  int    `gorm:"not null"`
}

错误4:bool 类型存储的困惑 ​

go
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 识别

练习题 ​

  1. 定义一个 Product 模型,包含字段:ID、商品名(Name)、价格(Price,float64)、库存(Stock,int)、创建时间、更新时间。使用 gorm.Model。
  2. 在上一题的基础上,给 Product 模型添加合适的标签:Name 不能为空且唯一、Price 类型为 decimal(10,2)、Stock 默认值为 0。
  3. 写一个完整的 Go 程序:定义模型 → 连接数据库 → AutoMigrate → 验证表结构。
  4. gorm.Model 包含哪四个字段?分别有什么作用?
  5. GORM 默认将 UserDetail 结构体映射为什么表名?为什么?
  6. (思考题)如果一个字段名是 IPAddress,GORM 默认会映射为什么列名?如果你希望映射为 ip_address,需要怎么做?

把零散知识,整理成可以反复走的路。