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

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 等主流数据库)

安装依赖

bash 复制代码
# 安装 GORM 核心库
go get gorm.io/gorm
# 安装 MySQL 驱动(根据数据库类型选择对应驱动)
go get gorm.io/driver/mysql

连接 MySQL 数据库

go 复制代码
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 四个字段,自动管理创建时间、更新时间、软删除,推荐直接嵌入自定义模型。
go 复制代码
// 嵌入 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 只会新增列和索引,不会删除或修改现有列

go 复制代码
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 处理冲突。

单条创建

go 复制代码
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 条

go 复制代码
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 使用数据库默认值

go 复制代码
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 的用户

go 复制代码
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 的用户

go 复制代码
var users []User
DB.Where("age > ? AND status = ?", 18, 1).Find(&users)
fmt.Printf("条件查询结果:%+v\n", users)

模糊查询: 查询用户名包含 "zhang" 的用户

go 复制代码
var searchUsers []User
DB.Where("username LIKE ?", "%zhang%").Find(&searchUsers)

IN 查询: 查询 ID 在 [1, 2, 3] 中的用户

go 复制代码
var inUsers []User
DB.Where("id IN ?", []int{1, 2, 3}).Find(&inUsers)

结构体/Map 条件查询:

  • 结构体条件:仅查询非零值字段
  • Map 条件:可查询零值字段
go 复制代码
var structUsers []User

DB.Where(&User{Age: 20, Status: 1}).Find(&structUsers)

DB.Where(map[string]any{"age": 20, "status": 1}).Find(&structUsers)

分页查询

go 复制代码
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)

查询单列

go 复制代码
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

go 复制代码
DB.Model(&User{}).Where("id = ?", 1).Update("age", 25)

更新多个字段(结构体)

go 复制代码
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,可更新零值)

go 复制代码
// Map 更新:可更新零值字段(如 Status=0)
DB.Model(&User{}).Where("id = ?", 1).Updates(map[string]any{
        "username": "zhangsan_map",
        "age":      30,
        "status":   0, // 零值也会更新
})

仅更新/忽略指定字段:

go 复制代码
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 字段

go 复制代码
DB.Delete(&User{}, 1)

// 此时查询 ID=1 的用户会返回 ErrRecordNotFound
var user User
if err := DB.First(&user, 1).Error; err == gorm.ErrRecordNotFound {
        fmt.Println("用户已软删除,查询不到")
}

查询所有数据:(包括软删除)

go 复制代码
var allUsers []User
DB.Unscoped().Find(&allUsers)
fmt.Printf("所有用户(包括软删除):%+v\n", allUsers)

物理删除:(真正删除)

go 复制代码
// 永久删除 ID=1 的用户
DB.Unscoped().Delete(&User{}, 1)

关联查询(解决 N+1 问题)

关联查询是企业开发的高频需求,GORM 支持一对一、一对多、多对多等关联关系,通过 PreloadJoins 可轻松实现关联查询,避免 N+1 查询问题。

模型定义(一对多示例)

以「用户 - 文章」为例,一个用户可以有多篇文章,一篇文章属于一个用户,这是典型的一对多关系。

go 复制代码
// 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):查询用户及其所有文章(一对多)

go 复制代码
var user User

// Preload("Articles") 会自动查询该用户的所有文章
DB.Preload("Articles").First(&user, 1)
fmt.Printf("用户:%+v,文章:%+v\n", user.Username, user.Articles)

预加载文章及其所属用户(多对一)

go 复制代码
var article Article
DB.Preload("User").First(&article, 1)
fmt.Printf("文章:%+v,作者:%+v\n", article.Title, article.User.Username)

嵌套预加载:查询用户、用户的文章、文章的评论

go 复制代码
// 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,代码更简洁

简化事务写法

go 复制代码
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 删除后 记录删除日志

自动加密密码

go 复制代码
func (u *User) BeforeCreate(tx *gorm.DB) error {
	// 模拟密码加密(实际项目中使用 bcrypt 等加密库)
	u.Password = "encrypted_" + u.Password
	fmt.Println("BeforeCreate 被调用,密码已加密")
	return nil
}

记录日志

go 复制代码
func (u *User) AfterCreate(tx *gorm.DB) error {
	fmt.Printf("AfterCreate 被调用,用户 %s 创建成功\n", u.Username)
	return nil
}
相关推荐
liqianpin11 天前
MySQL官网驱动下载(jar包驱动和ODBC驱动)【详细教程】
数据库·mysql
想唱rap1 天前
Linux线程
java·linux·运维·服务器·开发语言·mysql
Ricky_Theseus1 天前
SQL Server 的五种约束类型
数据库·sql·oracle
yige451 天前
【MySQL】MySQL内置函数--日期函数字符串函数数学函数其他相关函数
android·mysql·adb
星辰_mya1 天前
InnoDB的“身体结构”:页、Buffer Pool与Redo Log的底层奥秘
数据库·mysql·spring·面试·系统架构
下次一定x1 天前
深度解析 Kratos 客户端服务发现与负载均衡:从 Dial 入口到 gRPC 全链路落地(下篇)
后端·go
Rysxt_1 天前
MySQL 触发器详解与 Navicat 实战操作指南
mysql·触发器·navicat
XXOOXRT1 天前
Ubuntu搭建Java项目运行环境(JDK17+MySQL8.0)超详细教程
java·linux·mysql·ubuntu
tianyuanwo1 天前
MySQL 深度解析:从核心概念到实战指南,及数据库选型决策
数据库·mysql·centos
Wzx1980121 天前
gin_gorm
gin