GORM 实战入门:从环境搭建到企业级常用特性全解析
引言
GORM 是 Go 语言生态中最主流的 ORM框架,它将数据库表与 Go 结构体完美映射,让开发者无需手写 SQL 即可完成绝大多数 CRUD 操作,同时具备性能优秀、API 简洁、功能完善、生态丰富的特点。
- 模型定义:结构体与数据库表的映射,Tag 的使用。
- CRUD 操作:Create、Query、Update、Delete 的常用 API。
- 进阶功能:关联查询、事务、钩子函数,解决企业级复杂场景。
环境搭建 & 连接数据库
- Go 版本 ≥ 1.16
- 已安装数据库(本文以 MySQL 8.0 为例,GORM 同时支持 PostgreSQL、SQLite、SQL Server 等主流数据库)
安装依赖
# 安装 GORM 核心库
go get gorm.io/gorm
# 安装 MySQL 驱动(根据数据库类型选择对应驱动)
go get gorm.io/driver/mysql
连接 MySQL 数据库
package main
import (
"fmt"
"log"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// 全局 DB 实例,业务代码中直接调用
var DB *gorm.DB
// InitDB 初始化数据库连接
func InitDB() {
// 1. 配置 DSN(数据源名称)
// 语法:用户名:密码@tcp(主机:端口)/数据库名?charset=utf8mb4&parseTime=True&loc=Local
// 注意:parseTime=True 必须配置,否则无法处理时间类型;loc=Local 使用本地时区
dsn := "root:your_password@tcp(127.0.0.1:3306)/gorm_demo?charset=utf8mb4&parseTime=True&loc=Local"
// 2. 打开数据库连接
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
// 配置日志:开发环境用 logger.Info,生产环境用 logger.Silent 或 logger.Warn
Logger: logger.Default.LogMode(logger.Info),
// 禁用默认事务(GORM 默认单条 CRUD 也会开启事务,可禁用提升性能)
SkipDefaultTransaction: true,
// 命名策略:表名、列名的映射规则(可选,默认驼峰转下划线)
NamingStrategy: gorm.NamingStrategy{
SingularTable: true, // 表名使用单数(默认复数,如 user 表默认 users)
},
})
if err != nil {
log.Fatalf("数据库连接失败:%v", err)
}
// 3. 配置连接池(生产环境必配,性能优化核心)
sqlDB, err := DB.DB()
if err != nil {
log.Fatalf("获取数据库实例失败:%v", err)
}
// 最大空闲连接数:保持连接的空闲数量,避免频繁创建连接
sqlDB.SetMaxIdleConns(10)
// 最大打开连接数:同时存在的最大连接数,避免连接过多压垮数据库
sqlDB.SetMaxOpenConns(100)
// 连接最大存活时间:超过该时间的连接会被关闭
sqlDB.SetConnMaxLifetime(time.Hour)
// 连接最大空闲时间:超过该时间的空闲连接会被关闭
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
fmt.Println("数据库连接成功!")
}
func main() {
// 初始化数据库
InitDB()
}
- DSN 配置:
parseTime=True和loc=Local是 MySQL 连接的必配项,否则无法正确处理时间类型。 - 连接池配置:
SetMaxOpenConns和SetMaxIdleConns是生产环境性能优化的核心,需根据数据库性能和并发量调整。 - 日志配置:开发环境开启
logger.Info可打印 SQL 语句,方便调试;生产环境建议关闭或仅打印 Warn/Error 级别日志。
模型定义:结构体与数据库表的映射
GORM 通过 Go 结构体(Struct)定义数据库表结构,结构体字段对应表列,结构体标签(Tag)定义列的属性,这是 GORM 的核心基础。
模型定义规则:
- 表名映射:默认结构体名驼峰转下划线复数(如
User→users),可通过gorm.NamingStrategy{SingularTable: true}改为单数。 - 列名映射:默认结构体字段名驼峰转下划线(如
UserName→user_name),可通过gorm:"column:自定义列名"覆盖。 - 主键:默认字段名为
ID或Id的字段为主键,可通过gorm:"primaryKey"自定义主键。 - gorm.Model:GORM 内置的基础模型,包含
ID、CreatedAt、UpdatedAt、DeletedAt四个字段,自动管理创建时间、更新时间、软删除,推荐直接嵌入自定义模型。
// 嵌入 gorm.Model 自动获得 ID、CreatedAt、UpdatedAt、DeletedAt 字段
type User struct {
gorm.Model
// 用户名:唯一、非空、最大长度50
// gorm tag 语法:gorm:"属性1:值1;属性2:值2"
Username string `gorm:"column:username;type:varchar(50);uniqueIndex;not null;comment:用户名"`
// 密码:非空、最大长度255
Password string `gorm:"column:password;type:varchar(255);not null;comment:密码"`
// 邮箱:唯一、最大长度100
Email string `gorm:"column:email;type:varchar(100);uniqueIndex;comment:邮箱"`
// 年龄:默认值18、无符号
Age uint `gorm:"column:age;type:int unsigned;default:18;comment:年龄"`
// 状态:默认值1(1-正常 0-禁用)
Status int8 `gorm:"column:status;type:tinyint;default:1;comment:状态"`
// 生日:可为空
Birthday *time.Time `gorm:"column:birthday;type:date;comment:生日"`
}
// TableName 自定义表名(可选,优先级高于 NamingStrategy)
// 实现 Tabler 接口即可自定义表名
func (User) TableName() string {
return "sys_user"
}
常用 gorm Tag 详解
| Tag 属性 | 作用 | 示例 |
|---|---|---|
column | 自定义列名(MySQL表中的列名) | gorm:"column:user_name" |
type | 定义列类型 | gorm:"type:varchar(50)" |
primaryKey | 标记为主键 | gorm:"primaryKey" |
uniqueIndex | 唯一索引 | gorm:"uniqueIndex" |
index | 普通索引 | gorm:"index" |
not null | 非空约束 | gorm:"not null" |
default | 默认值 | gorm:"default:18" |
comment | 列注释 | gorm:"comment:用户名" |
autoIncrement | 自增(默认主键自增) | gorm:"autoIncrement" |
- | 忽略该字段(不映射到数据库) | gorm:"-" |
自动迁移 根据 User 模型创建或更新表结构
AutoMigrate 只会新增列和索引,不会删除或修改现有列
err := DB.AutoMigrate(&User{})
if err != nil {
log.Fatalf("自动迁移失败:%v", err)
}
CRUD 实战
创建
| API | 作用 | 特点 |
|---|---|---|
DB.Create(&obj) | 创建单条记录 | 自动填充 CreatedAt、UpdatedAt,返回自增 ID |
DB.CreateInBatches(&objs, batchSize) | 批量创建 | 批量插入,性能更高,batchSize 为每批数量 |
DB.Select("字段1", "字段2").Create(&obj) | 创建时仅插入指定字段 | 忽略未选中的字段,使用数据库默认值 |
DB.Omit("字段1", "字段2").Create(&obj) | 创建时忽略指定字段 | 不插入指定字段,使用数据库默认值 |
- 必须传指针:
Create方法必须传入结构体指针,否则无法填充自增 ID、CreatedAt等字段。 - 唯一索引冲突:如果模型有唯一索引(如
Username),重复插入会报错,需提前判断或使用Clauses处理冲突。
单条创建
user := User{
Username: "zhangsan",
Password: "123456",
Email: "zhangsan@example.com",
Age: 20,
}
if err := DB.Create(&user).Error; err != nil {
log.Printf("创建用户失败:%v", err)
return
}
fmt.Printf("创建用户成功,ID:%d\n", user.ID)
批量创建: 每批插入 2 条
users := []User{
{Username: "lisi", Password: "123456", Email: "lisi@example.com"},
{Username: "wangwu", Password: "123456", Email: "wangwu@example.com"},
{Username: "zhaoliu", Password: "123456", Email: "zhaoliu@example.com"},
}
if err := DB.CreateInBatches(users, 2).Error; err != nil {
log.Printf("批量创建用户失败:%v", err)
return
}
fmt.Println("批量创建用户成功")
创建时仅插入指定字段: 仅插入 Username 和 Password,Email 和 Age 使用数据库默认值
user2 := User{
Username: "sunqi",
Password: "123456",
Email: "sunqi@example.com",
Age: 25,
}
DB.Select("Username", "Password").Create(&user2)
查询
| API | 作用 | 示例 |
|---|---|---|
First(&obj, id) | 查询第一条记录(按主键升序) | DB.First(&user, 1) |
Take(&obj) | 查询第一条记录(无排序) | DB.Take(&user) |
Find(&objs) | 查询所有记录 | DB.Find(&users) |
Where(条件, 参数) | 条件查询 | DB.Where("age > ?", 18).Find(&users) |
Select(字段) | 查询指定字段 | DB.Select("id, username").Find(&users) |
Order(排序规则) | 排序 | DB.Order("age desc, id asc").Find(&users) |
Limit(n) | 限制查询数量 | DB.Limit(10).Find(&users) |
Offset(n) | 偏移量(分页用) | DB.Offset(0).Limit(10).Find(&users) |
Count(&count) | 统计数量 | DB.Model(&User{}).Count(&count) |
Pluck(字段, &slice) | 查询单列并返回切片 | DB.Model(&User{}).Pluck("username", &usernames) |
主键查询: 查询 ID=1 的用户
var user User
if err := DB.First(&user, 1).Error; err != nil {
if err == gorm.ErrRecordNotFound {
fmt.Println("用户不存在")
} else {
log.Printf("查询失败:%v", err)
}
return
}
fmt.Printf("查询到用户:%+v\n", user)
条件查询: 查询年龄大于 18 且状态为 1 的用户
var users []User
DB.Where("age > ? AND status = ?", 18, 1).Find(&users)
fmt.Printf("条件查询结果:%+v\n", users)
模糊查询: 查询用户名包含 "zhang" 的用户
var searchUsers []User
DB.Where("username LIKE ?", "%zhang%").Find(&searchUsers)
IN 查询: 查询 ID 在 [1, 2, 3] 中的用户
var inUsers []User
DB.Where("id IN ?", []int{1, 2, 3}).Find(&inUsers)
结构体/Map 条件查询:
- 结构体条件:仅查询非零值字段
- Map 条件:可查询零值字段
var structUsers []User
DB.Where(&User{Age: 20, Status: 1}).Find(&structUsers)
DB.Where(map[string]any{"age": 20, "status": 1}).Find(&structUsers)
分页查询
var pageUsers []User
var total int64
page := 1 // 页码
pageSize := 2 // 每页数量
// 先统计总数
DB.Model(&User{}).Count(&total)
// 再查询分页数据
DB.Offset((page - 1) * pageSize).Limit(pageSize).Order("id desc").Find(&pageUsers)
fmt.Printf("分页查询:总数=%d,数据=%+v\n", total, pageUsers)
查询单列
var usernames []string
DB.Model(&User{}).Pluck("username", &usernames)
fmt.Printf("用户名列表:%v\n", usernames)
-
First vs Take vs Find:
First:按主键升序查询第一条,找不到返回gorm.ErrRecordNotFoundTake:查询第一条(无排序),找不到返回gorm.ErrRecordNotFoundFind:查询所有,找不到不报错,返回空切片
-
零值查询问题:使用结构体作为
Where条件时,GORM 会忽略零值字段(如0、""、false),如需查询零值,需使用 Map 或原生 SQL。
更新
| API | 作用 | 特点 |
|---|---|---|
DB.Save(&obj) | 保存(创建或更新) | 根据主键判断,存在则更新,不存在则创建,会更新所有字段 |
DB.Model(&obj).Update("字段", 值) | 更新单个字段 | 仅更新指定字段,自动填充 UpdatedAt |
DB.Model(&obj).Updates(map/struct) | 更新多个字段 | 结构体仅更新非零值字段,Map 可更新零值 |
DB.Model(&obj).Select("字段1", "字段2").Updates(...) | 仅更新指定字段 | 配合 Updates 使用,限制更新范围 |
DB.Model(&obj).Omit("字段1", "字段2").Updates(...) | 忽略指定字段不更新 | 配合 Updates 使用,排除不需要更新的字段 |
根据主键更新单个字段: 更新 ID=1 的用户的 Age 为 25
DB.Model(&User{}).Where("id = ?", 1).Update("age", 25)
更新多个字段(结构体)
var user User
DB.First(&user, 1)
// 结构体更新:仅更新非零值字段(如 Status=0 不会被更新)
user.Username = "zhangsan_new"
user.Email = "zhangsan_new@example.com"
DB.Model(&user).Updates(user)
更新多个字段(Map,可更新零值)
// Map 更新:可更新零值字段(如 Status=0)
DB.Model(&User{}).Where("id = ?", 1).Updates(map[string]any{
"username": "zhangsan_map",
"age": 30,
"status": 0, // 零值也会更新
})
仅更新/忽略指定字段:
DB.Model(&User{}).Where("id = ?", 1).
Select("Username", "Email"). // 仅更新这两个字段
Updates(map[string]any{
"username": "zhangsan_select",
"email": "zhangsan_select@example.com",
"age": 35, // 不会被更新
})
-
Save vs Updates:
Save:会更新所有字段(包括零值),即使字段未修改,慎用!Updates:仅更新指定字段,推荐优先使用
-
结构体更新零值问题:使用结构体更新时,零值字段会被忽略,如需更新零值,必须使用 Map。
删除
GORM 默认支持软删除(嵌入 gorm.Model 后自动启用),删除时不会真正删除数据,而是将 DeletedAt 字段设为当前时间,查询时自动过滤已软删除的数据。
| API | 作用 | 特点 |
|---|---|---|
DB.Delete(&obj, id) | 软删除(默认) | 仅设置 DeletedAt,查询时自动过滤 |
DB.Unscoped().Delete(&obj, id) | 物理删除 | 真正从数据库删除数据 |
DB.Unscoped().Find(&objs) | 查询所有数据(包括软删除) | 可查询已软删除的记录 |
- 软删除的表唯一索引需包含
DeletedAt,否则软删除后无法插入相同唯一键的数据。 - 大数据量场景慎用软删除,会导致表数据量持续增大,影响查询性能。
软删除: 删除 ID=1 的用户,实际是设置 DeletedAt 字段
DB.Delete(&User{}, 1)
// 此时查询 ID=1 的用户会返回 ErrRecordNotFound
var user User
if err := DB.First(&user, 1).Error; err == gorm.ErrRecordNotFound {
fmt.Println("用户已软删除,查询不到")
}
查询所有数据:(包括软删除)
var allUsers []User
DB.Unscoped().Find(&allUsers)
fmt.Printf("所有用户(包括软删除):%+v\n", allUsers)
物理删除:(真正删除)
// 永久删除 ID=1 的用户
DB.Unscoped().Delete(&User{}, 1)
关联查询(解决 N+1 问题)
关联查询是企业开发的高频需求,GORM 支持一对一、一对多、多对多等关联关系,通过 Preload 或 Joins 可轻松实现关联查询,避免 N+1 查询问题。
模型定义(一对多示例)
以「用户 - 文章」为例,一个用户可以有多篇文章,一篇文章属于一个用户,这是典型的一对多关系。
// Article 文章模型
type Article struct {
gorm.Model
Title string `gorm:"column:title;type:varchar(100);not null;"`
Content string `gorm:"column:content;type:text;"`
UserID uint `gorm:"column:user_id;type:int unsigned;not null;index;"` // 外键
User User `gorm:"foreignKey:UserID"` // 关联用户模型
}
func (Article) TableName() string {
return "sys_article"
}
func main() {
InitDB()
DB.AutoMigrate(&User{}, &Article{}) // 自动迁移
}
| API | 作用 | 特点 |
|---|---|---|
Preload("关联字段") | 预加载关联数据 | 分两次查询(先查主表,再查关联表),避免 N+1,性能优秀 |
Joins("关联表") | 连接查询 | 一次查询(JOIN),适合需要关联条件的场景 |
预加载(Preload):查询用户及其所有文章(一对多)
var user User
// Preload("Articles") 会自动查询该用户的所有文章
DB.Preload("Articles").First(&user, 1)
fmt.Printf("用户:%+v,文章:%+v\n", user.Username, user.Articles)
预加载文章及其所属用户(多对一)
var article Article
DB.Preload("User").First(&article, 1)
fmt.Printf("文章:%+v,作者:%+v\n", article.Title, article.User.Username)
嵌套预加载:查询用户、用户的文章、文章的评论
// 1个用户 → 多篇文章
type User struct {
gorm.Model
Articles []Article `gorm:"foreignKey:UserID"` // 1:n
}
// 1篇文章 → 多条评论
type Article struct {
gorm.Model
UserID uint
User User `gorm:"foreignKey:UserID"`
Comments []Comment `gorm:"foreignKey:ArticleID"` // 1:n
}
type Comment struct {
gorm.Model
ArticleID uint
Content string
}
DB.Preload("Articles.Comments").First(&user, 1)
事务
事务 = 一组操作,要么全部成功,要么全部失败,不会只做一半。 (原子性\一致性)
- 涉及多张表的写操作必须使用事务(如创建订单 + 扣库存)。
- 单表批量写操作建议使用事务,提升性能。
| API | 作用 | 特点 |
|---|---|---|
DB.Begin() | 开启事务 | 返回事务对象 tx,后续操作使用 tx 而非 DB |
tx.Commit() | 提交事务 | 所有操作成功后提交,数据生效 |
tx.Rollback() | 回滚事务 | 任何一步失败都要回滚,撤销所有操作 |
DB.Transaction(func(tx *gorm.DB) error) | 简化事务 | 自动处理 Begin/Commit/Rollback,代码更简洁 |
简化事务写法
func TransactionDemo() {
// 模拟场景:创建用户的同时创建一篇文章,要么都成功,要么都失败
err := DB.Transaction(func(tx *gorm.DB) error {
// 1. 创建用户(注意:必须使用 tx,不能用 DB!)
user := User{Username: "transaction_user", Password: "123456"}
if err := tx.Create(&user).Error; err != nil {
// 返回错误会自动回滚
return err
}
// 2. 创建文章(关联刚才创建的用户)
article := Article{
Title: "事务测试文章",
Content: "这是事务测试内容",
UserID: user.ID,
}
if err := tx.Create(&article).Error; err != nil {
// 返回错误会自动回滚
return err
}
// 返回 nil 会自动提交
return nil
})
if err != nil {
log.Printf("事务执行失败,已回滚:%v", err)
return
}
fmt.Println("事务执行成功!")
}
- 必须使用 tx:事务内的所有 CRUD 操作必须使用
tx对象,不能使用全局DB,否则事务不生效。 - 错误处理:事务函数内返回任何错误都会自动回滚,返回
nil自动提交。
钩子函数(Hook):自动处理业务逻辑
GORM 提供了丰富的钩子函数,在 CRUD 操作的特定时机自动调用,适合处理自动填充、数据校验、日志记录等通用逻辑。
| 钩子函数 | 调用时机 | 示例场景 |
|---|---|---|
BeforeCreate | 创建前 | 自动加密密码、生成唯一 ID |
AfterCreate | 创建后 | 记录创建日志、发送通知 |
BeforeUpdate | 更新前 | 自动更新 UpdatedAt、数据校验 |
AfterUpdate | 更新后 | 记录更新日志 |
BeforeDelete | 删除前 | 数据校验、删除关联数据 |
AfterDelete | 删除后 | 记录删除日志 |
自动加密密码
func (u *User) BeforeCreate(tx *gorm.DB) error {
// 模拟密码加密(实际项目中使用 bcrypt 等加密库)
u.Password = "encrypted_" + u.Password
fmt.Println("BeforeCreate 被调用,密码已加密")
return nil
}
记录日志
func (u *User) AfterCreate(tx *gorm.DB) error {
fmt.Printf("AfterCreate 被调用,用户 %s 创建成功\n", u.Username)
return nil
}