Skip to content

01-GORM 简介与模型定义

GORM 是 Go 语言中最流行的 ORM(对象关系映射)库,它将结构体与数据库表建立映射关系,让开发者可以用 Go 代码操作数据库而无需手写大量 SQL。本篇将从安装初始化讲起,深入到模型定义、字段标签、模型约定与 AutoMigrate,最终通过一个完整的博客系统模型示例把所有知识点串起来。

GORM 简介

GORM 是一个开发者友好的 Go 语言 ORM 库,自 2013 年发布以来已经成为 Go 生态中事实上的 ORM 标准。它的主要特性包括:

  • 全功能 ORM:支持增删改查、关联关系、预加载、事务等
  • 链式 API:可复用的 Scopes、灵活的查询构造器
  • 钩子(Hook)机制:BeforeCreate、AfterUpdate 等生命周期回调
  • 自动迁移:AutoMigrate 自动同步表结构
  • 多数据库驱动:MySQL、PostgreSQL、SQLite、SQL Server
  • 高级特性:软删除、复合主键、乐观锁、多态关联
  • 插件生态:分页、读写分离、Prometheus 监控、OpenTracing

官方网站:https://gorm.io

GORM 的设计哲学是"开发者友好"——它隐藏了大部分样板代码,让你专注于业务逻辑;同时又足够灵活,遇到复杂场景可以随时回退到原生 SQL。

安装与初始化

安装 GORM

GORM 的核心库是 gorm.io/gorm,它本身不包含任何数据库驱动,需要根据实际使用的数据库安装对应的驱动包。

bash
# 安装 GORM 核心
go get -u gorm.io/gorm

# 安装 MySQL 驱动
go get -u gorm.io/driver/mysql

# 安装 PostgreSQL 驱动
go get -u gorm.io/driver/postgres

# 安装 SQLite 驱动(开发调试很方便)
go get -u gorm.io/driver/sqlite

初始化项目

创建一个新项目并初始化 go.mod

bash
mkdir gorm-demo
cd gorm-demo
go mod init gorm-demo
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

连接数据库

连接 MySQL

MySQL 是最常用的关系型数据库,下面演示如何用 GORM 连接 MySQL:

go
package main

import (
	"fmt"
	"log"
	"os"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

func InitMySQL() (*gorm.DB, error) {
	// DSN 格式:用户名:密码@tcp(主机:端口)/数据库名?参数
	dsn := "root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: logger.New(
			log.New(os.Stdout, "\r\n", log.LstdFlags),
			logger.Config{
				SlowThreshold:             200 * time.Millisecond,
				LogLevel:                  logger.Warn,
				IgnoreRecordNotFoundError: true,
				Colorful:                  true,
			},
		),
	})
	if err != nil {
		return nil, fmt.Errorf("连接 MySQL 失败: %w", err)
	}
	return db, nil
}

func main() {
	db, err := InitMySQL()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("MySQL 连接成功:", db)
}

DSN 各参数含义:

  • charset=utf8mb4:字符集,支持 emoji 等 4 字节字符
  • parseTime=True:自动解析 MySQL 时间类型为 Go 的 time.Time
  • loc=Local:使用本地时区

连接 PostgreSQL

PostgreSQL 在复杂业务场景下也很常见,连接方式类似:

go
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

func InitPostgres() (*gorm.DB, error) {
	dsn := "host=127.0.0.1 user=postgres password=123456 dbname=demo port=5432 sslmode=disable TimeZone=Asia/Shanghai"
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		return nil, fmt.Errorf("连接 PostgreSQL 失败: %w", err)
	}
	return db, nil
}

func main() {
	db, err := InitPostgres()
	if err != nil {
		log.Fatal(err)
	}
	// 简单 ping 一下
	sqlDB, _ := db.DB()
	_ = sqlDB.Ping()
	fmt.Println("PostgreSQL 连接成功,时间:", time.Now())
}

连接 SQLite

SQLite 是单文件数据库,非常适合开发、测试、单机工具:

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func InitSQLite() (*gorm.DB, error) {
	// 使用文件数据库,也可以使用 ":memory:" 创建内存数据库
	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		return nil, fmt.Errorf("连接 SQLite 失败: %w", err)
	}
	return db, nil
}

func main() {
	db, err := InitSQLite()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("SQLite 连接成功:", db.Name())
}

连接池配置

无论使用哪种数据库,生产环境都必须配置连接池,否则容易出现连接耗尽或连接失效等问题。GORM 通过 db.DB() 返回底层的 *sql.DB,再调用其方法配置连接池。

go
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func InitDBWithPool() (*gorm.DB, error) {
	dsn := "root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		return nil, err
	}

	sqlDB, err := db.DB()
	if err != nil {
		return nil, err
	}

	// 最大空闲连接数:建议设置为最大连接数的 1/3 到 1/2
	sqlDB.SetMaxIdleConns(10)

	// 最大打开连接数:根据数据库服务器配置和并发量设置
	// 注意:不要超过数据库服务器 max_connections 配置
	sqlDB.SetMaxOpenConns(100)

	// 连接最大存活时间:建议小于数据库 wait_timeout
	// MySQL 默认 wait_timeout 是 8 小时,这里设为 1 小时
	sqlDB.SetConnMaxLifetime(time.Hour)

	// 连接最大空闲时间:超过这个时间的空闲连接会被关闭
	// 建议设置为几分钟,避免使用过期连接
	sqlDB.SetConnMaxIdleTime(10 * time.Minute)

	return db, nil
}

func main() {
	db, err := InitDBWithPool()
	if err != nil {
		log.Fatal(err)
	}
	sqlDB, _ := db.DB()
	stats := sqlDB.Stats()
	fmt.Printf("最大空闲连接: %d, 最大打开连接: %d\n",
		stats.MaxIdleConnections, stats.MaxOpenConnections)
}

连接池参数调优建议:

  • MaxIdleConns:太小会导致频繁建立连接,太大会占用资源,建议 10-20
  • MaxOpenConns:根据数据库性能和并发量设置,常见 100-500
  • ConnMaxLifetime:必须小于数据库 wait_timeout,避免连接失效
  • ConnMaxIdleTime:保持空闲连接新鲜,避免被数据库单方面关闭

模型定义基础

GORM 使用结构体(struct)映射数据库表,结构体字段映射表列。默认情况下,GORM 将结构体名转换为蛇形(snake_case)作为表名,字段名转换为蛇形作为列名。

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// User 结构体映射 users 表
type User struct {
	ID        uint   // 默认作为主键
	Name      string // 映射到 name 列
	Email     string // 映射到 email 列
	Age       int    // 映射到 age 列
	CreatedAt string // 映射到 created_at 列(非内置类型)
}

func main() {
	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	// 自动迁移:根据 User 结构体创建 users 表
	if err := db.AutoMigrate(&User{}); err != nil {
		log.Fatal(err)
	}

	// 插入一条记录
	user := User{Name: "张三", Email: "zhangsan@example.com", Age: 25}
	result := db.Create(&user)
	fmt.Printf("插入 %d 行, ID: %d\n", result.RowsAffected, user.ID)
}

字段标签

通过结构体 tag 可以精细控制字段的映射规则、类型、约束等。GORM 的 tag 以 gorm: 开头,多个配置用 ; 分隔。

go
package main

import (
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Product struct {
	// column 指定列名;primaryKey 指定主键
	ID uint `gorm:"column:id;primaryKey"`

	// type 指定列类型;size 指定长度;not null 非空约束
	Name string `gorm:"column:name;type:varchar(100);size:100;not null"`

	// default 指定默认值;comment 添加注释
	Status int `gorm:"column:status;default:1;comment:状态:1启用 0禁用"`

	// unique 唯一约束;index 创建索引
	Email string `gorm:"column:email;type:varchar(150);uniqueIndex"`

	// autoCreateTime 自动记录创建时间(秒)
	CreatedAt int64 `gorm:"column:created_at;autoCreateTime"`

	// autoUpdateTime 自动记录更新时间(秒)
	UpdatedAt int64 `gorm:"column:updated_at;autoUpdateTime"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Product{})
}

常用字段标签汇总:

标签说明示例
column列名gorm:"column:user_name"
type列类型gorm:"type:varchar(100)"
size字段长度gorm:"size:255"
primaryKey主键gorm:"primaryKey"
autoIncrement自增gorm:"autoIncrement"
not null非空gorm:"not null"
default默认值gorm:"default:0"
unique唯一约束gorm:"unique"
index普通索引gorm:"index"
uniqueIndex唯一索引gorm:"uniqueIndex"
comment注释gorm:"comment:用户名"
autoCreateTime自动创建时间gorm:"autoCreateTime"
autoUpdateTime自动更新时间gorm:"autoUpdateTime"

模型约定

GORM 有一些默认约定,能减少大量样板代码:

  • ID 字段默认作为主键
  • CreatedAt 自动记录记录创建时间
  • UpdatedAt 自动记录记录更新时间
  • DeletedAt 软删除字段(gorm.DeletedAt 类型)
go
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// BaseModel 内嵌基础模型,复用约定字段
type BaseModel struct {
	ID        uint           `gorm:"primaryKey"`
	CreatedAt time.Time      // 自动创建时间
	UpdatedAt time.Time      // 自动更新时间
	DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除字段
}

type Article struct {
	BaseModel // 内嵌,继承 ID、CreatedAt、UpdatedAt、DeletedAt
	Title    string
	Content  string
}

func main() {
	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Article{})

	// 创建:CreatedAt 和 UpdatedAt 自动填充
	article := Article{Title: "Hello GORM", Content: "GORM 模型约定示例"}
	db.Create(&article)
	fmt.Printf("创建时间: %s, 更新时间: %s\n", article.CreatedAt, article.UpdatedAt)

	// 更新:UpdatedAt 自动更新
	article.Title = "Hello GORM Updated"
	db.Save(&article)
	fmt.Printf("更新后 UpdatedAt: %s\n", article.UpdatedAt)

	// 软删除:Delete 不会真正删除,而是设置 DeletedAt
	db.Delete(&article)
	// 普通查询找不到已软删除的记录
	var count int64
	db.Model(&Article{}).Count(&count)
	fmt.Printf("普通查询数量: %d\n", count)

	// 使用 Unscoped 可以查询到软删除的记录
	var allArticles []Article
	db.Unscoped().Find(&allArticles)
	fmt.Printf("包含软删除的查询数量: %d\n", len(allArticles))
}

软删除的优势:

  • 误删数据可以恢复
  • 满足审计合规要求
  • 避免外键级联删除导致的连锁问题

自定义表名

如果默认的蛇形命名不满足需求,可以实现 Tabler 接口自定义表名:

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Person struct {
	ID   uint
	Name string
}

// TableName 自定义表名,实现 Tabler 接口
func (Person) TableName() string {
	return "t_person" // 自定义表名前缀
}

func main() {
	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	// 禁用默认表名复数形式(默认 persons 会变成 people)
	// 全局禁用复数:
	// db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
	//     NamingStrategy: schema.NamingStrategy{SingularTable: true},
	// })

	db.AutoMigrate(&Person{})

	p := Person{Name: "Alice"}
	db.Create(&p)

	var found Person
	db.First(&found, p.ID)
	fmt.Println("查询到:", found.Name)
}

也可以临时指定表名:

go
// 查询时临时指定表名
db.Table("custom_users").Find(&users)

AutoMigrate 详解与注意事项

AutoMigrate 会根据模型结构创建表、缺失的列、缺失的索引。但它有一些重要限制需要注意:

AutoMigrate 会做的事:

  • 创建表(如果不存在)
  • 添加缺失的列
  • 添加缺失的索引
  • 修改列类型(部分数据库支持)

AutoMigrate 不会做的事:

  • 不会删除列(避免数据丢失)
  • 不会删除索引
  • 不会重命名列
  • 不会修改列的约束(如 NOT NULL)
go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Book struct {
	ID     uint   `gorm:"primaryKey"`
	Title  string `gorm:"type:varchar(200);not null"`
	Author string `gorm:"type:varchar(100)"`
	Price  float64
	ISBN   string `gorm:"uniqueIndex"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	// AutoMigrate 会创建 books 表
	if err := db.AutoMigrate(&Book{}); err != nil {
		log.Fatal("迁移失败:", err)
	}

	// 验证表是否创建
	migrator := db.Migrator()
	if migrator.HasTable(&Book{}) {
		fmt.Println("books 表已创建")
	}
	if migrator.HasColumn(&Book{}, "ISBN") {
		fmt.Println("isbn 列已创建")
	}
	if migrator.HasIndex(&Book{}, "ISBN") {
		fmt.Println("isbn 唯一索引已创建")
	}

	// 后续如果给 Book 增加新字段 Description
	// 再次 AutoMigrate 会自动添加该列
}

AutoMigrate 的注意事项:

  1. 生产环境慎用:AutoMigrate 适合开发期快速迭代,生产环境建议使用专业的迁移工具(如 golang-migrate)以获得版本控制和回滚能力
  2. 不会删列:删除模型字段后再次 AutoMigrate 不会删除数据库列,需要手动处理
  3. 类型变更有限:某些类型变更可能失败,需要手动 SQL
  4. 避免重复迁移:程序启动时重复 AutoMigrate 同一个表是低效的,建议有条件地执行

完整示例:博客系统模型定义

下面通过一个博客系统的模型定义综合运用以上知识,包含 User、Post、Comment 三个模型及其关联关系。

go
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// BaseModel 公共字段
type BaseModel struct {
	ID        uint           `gorm:"primaryKey" json:"id"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

// User 用户模型
type User struct {
	BaseModel
	Username string `gorm:"type:varchar(50);uniqueIndex;not null" json:"username"`
	Email    string `gorm:"type:varchar(150);uniqueIndex;not null" json:"email"`
	Password string `gorm:"type:varchar(100);not null" json:"-"` // json:"-" 不序列化
	Nickname string `gorm:"type:varchar(50)" json:"nickname"`
	Age      int    `gorm:"default:0" json:"age"`
	Status   int    `gorm:"default:1;comment:状态 1启用 0禁用" json:"status"`
	// 一对多:一个用户有多篇文章
	Posts []Post `gorm:"foreignKey:UserID" json:"posts,omitempty"`
}

// Post 文章模型
type Post struct {
	BaseModel
	Title    string `gorm:"type:varchar(200);not null;index" json:"title"`
	Content  string `gorm:"type:text" json:"content"`
	ViewCount int   `gorm:"default:0" json:"view_count"`
	// 多对一:文章属于某个用户
	UserID uint `gorm:"index;not null" json:"user_id"`
	User   User `gorm:"foreignKey:UserID" json:"user,omitempty"`
	// 一对多:一篇文章有多条评论
	Comments []Comment `gorm:"foreignKey:PostID" json:"comments,omitempty"`
}

// Comment 评论模型
type Comment struct {
	BaseModel
	Content string `gorm:"type:text;not null" json:"content"`
	PostID  uint   `gorm:"index;not null" json:"post_id"`
	Post    Post   `gorm:"foreignKey:PostID" json:"-"`
	UserID  uint   `gorm:"index;not null" json:"user_id"`
	User    User   `gorm:"foreignKey:UserID" json:"user,omitempty"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("blog.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	// 自动迁移所有模型(注意顺序,先迁移被引用的表)
	if err := db.AutoMigrate(&User{}, &Post{}, &Comment{}); err != nil {
		log.Fatal("迁移失败:", err)
	}

	// 创建用户
	user := User{
		Username: "alice",
		Email:    "alice@example.com",
		Password: "hashed_password",
		Nickname: "爱丽丝",
		Age:      28,
	}
	if err := db.Create(&user).Error; err != nil {
		log.Fatal(err)
	}
	fmt.Printf("用户创建成功,ID: %d\n", user.ID)

	// 创建文章
	post := Post{
		Title:    "GORM 入门指南",
		Content:  "GORM 是 Go 最流行的 ORM...",
		UserID:   user.ID,
	}
	if err := db.Create(&post).Error; err != nil {
		log.Fatal(err)
	}
	fmt.Printf("文章创建成功,ID: %d\n", post.ID)

	// 创建评论
	comment := Comment{
		Content: "写得很好!",
		PostID:  post.ID,
		UserID:  user.ID,
	}
	if err := db.Create(&comment).Error; err != nil {
		log.Fatal(err)
	}
	fmt.Printf("评论创建成功,ID: %d\n", comment.ID)

	// 预加载查询,演示关联
	var fullPost Post
	if err := db.Preload("User").Preload("Comments").First(&fullPost, post.ID).Error; err != nil {
		log.Fatal(err)
	}
	fmt.Printf("文章: %s, 作者: %s, 评论数: %d\n",
		fullPost.Title, fullPost.User.Nickname, len(fullPost.Comments))

	// 软删除演示
	db.Delete(&comment)
	var activeComments []Comment
	db.Where("post_id = ?", post.ID).Find(&activeComments)
	fmt.Printf("软删除后剩余评论数: %d\n", len(activeComments))
}

小结

本篇系统介绍了 GORM 的核心入门知识:

  1. GORM 是什么:Go 最流行的全功能 ORM,开发者友好且生态完善
  2. 安装与初始化:核心库 gorm.io/gorm 加上对应数据库驱动
  3. 多数据库连接:MySQL、PostgreSQL、SQLite 各有 DSN 格式
  4. 连接池配置:通过 db.DB() 获取底层 *sql.DB 配置四个关键参数
  5. 模型定义:结构体映射表,字段映射列,默认蛇形命名
  6. 字段标签gorm: tag 控制类型、约束、索引、注释等
  7. 模型约定:ID、CreatedAt、UpdatedAt、DeletedAt 四个内置字段
  8. 自定义表名:实现 Tabler 接口的 TableName() 方法
  9. AutoMigrate:自动建表加列,但不会删列、不会重命名,生产慎用
  10. 综合示例:博客系统 User-Post-Comment 三表模型与关联

下一篇我们将深入 CRUD 操作与查询基础,掌握 GORM 的核心数据操作能力。