GORM 实战入门:从环境搭建到企业级常用特性全解析

0 阅读13分钟

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=Trueloc=Local 是 MySQL 连接的必配项,否则无法正确处理时间类型。
  • 连接池配置SetMaxOpenConnsSetMaxIdleConns 是生产环境性能优化的核心,需根据数据库性能和并发量调整。
  • 日志配置:开发环境开启 logger.Info 可打印 SQL 语句,方便调试;生产环境建议关闭或仅打印 Warn/Error 级别日志。

模型定义:结构体与数据库表的映射

GORM 通过 Go 结构体(Struct)定义数据库表结构,结构体字段对应表列,结构体标签(Tag)定义列的属性,这是 GORM 的核心基础。

模型定义规则:

  • 表名映射:默认结构体名驼峰转下划线复数(如 Userusers),可通过 gorm.NamingStrategy{SingularTable: true} 改为单数。
  • 列名映射:默认结构体字段名驼峰转下划线(如 UserNameuser_name),可通过 gorm:"column:自定义列名" 覆盖。
  • 主键:默认字段名为 IDId 的字段为主键,可通过 gorm:"primaryKey" 自定义主键。
  • gorm.Model:GORM 内置的基础模型,包含 IDCreatedAtUpdatedAtDeletedAt 四个字段,自动管理创建时间、更新时间、软删除,推荐直接嵌入自定义模型。
// 嵌入 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)创建单条记录自动填充 CreatedAtUpdatedAt,返回自增 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.ErrRecordNotFound
    • Take:查询第一条(无排序),找不到返回 gorm.ErrRecordNotFound
    • Find:查询所有,找不到不报错,返回空切片
  • 零值查询问题:使用结构体作为 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 支持一对一、一对多、多对多等关联关系,通过 PreloadJoins 可轻松实现关联查询,避免 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
}