# 简单介绍
GORM (opens new window) 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server
- 安装
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
# 使用流程
# 基础使用
- 在 models 下面新建 core.go ,建立数据库链接
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// DB 是全局的数据库连接池实例
var DB *gorm.DB
func init() {
// 1. 修复拼写错误:loc=Local
dsn := "root:123456@tcp(127.0.0.1:3306)/gin?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
QueryFields: true,
SkipDefaultTransaction: true,
})
// 2. 错误判断必须提前:如果连接失败,db 可能是 nil,此时调用 db.Debug() 会 Panic
if err != nil {
panic(fmt.Errorf("fatal error: failed to connect database: %w", err))
}
// 3. 在这里注册全局插件 DataHooks,而不是注册具体的操作策略
// db.Use(&DataHooks{})
// 4. 统一赋值给全局变量,并开启 Debug
DB = db.Debug()
}
- 定义操作数据库的模型
// 结构体的名称必须首字母大写 ,并和数据库表名称对应
// 结构体中的字段名称首字母必须大写,并和数据库表中的字段一一对应
// 默认情况表名是结构体名称的复数形式。如果结构体名称定义成User,则模型默认操作的是 users 表
type User struct {
ID uint
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
ignored string
}
// 可以使用结构体中的自定义方法 TableName 改变结构体的默认表名称
// 使用匿名接收器,方法内部无法访问接收器对应的值,即不能使用 User 类型的字段或方法
func (User) TableName() string {
return "user" // 把 User 结构体默认操作的表改为 user 表
}
- 预定义的结构体 gorm.Model
// 可以直接在您的结构体中嵌入 gorm.Model
type Model struct {
ID uint
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
}
- 自定义的 GORM 插件(Plugin)
func InitDB(dsn string, opts ...func(*gorm.DB)) *gorm.DB {
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
// 遍历并执行所有传入的配置函数
for _, opt := range opts {
opt(db) // 这里就会执行匿名函数,从而注册插件
}
return db
}
// 调用时:
db := InitDB("user:pass@tcp(127.0.0.1:3306)/dbname", func(d *gorm.DB) {
d.Use(&DataHooks{}) // 注入自定义插件
})
// DataHooks 结构体必须实现 GORM Plugin 接口的两个方法
type DataHooks struct{}
// 1. 实现 Name 方法:返回插件的名称
func (d *DataHooks) Name() string {
return "data_hooks_plugin"
}
// 2. 实现 Initialize 方法:插件初始化时执行的逻辑
func (d *DataHooks) Initialize(db *gorm.DB) error {
// 在这里注册各种 Callbacks (回调/钩子)
// 例如:在创建数据前,自动填充 CreatedAt
db.Callback().Create().Before("gorm:create").Register("created_at", func(tx *gorm.DB) {
// ... 具体的钩子逻辑 ...
})
// 例如:在更新数据前,自动填充 UpdatedAt
db.Callback().Update().Before("gorm:update").Register("updated_at", func(tx *gorm.DB) {
// ... 具体的钩子逻辑 ...
})
return nil
}
- 标签的使用
type B struct {
Id int64 `gorm:"column:c_id;primarykey" json:"Id,omitempty"`
CreateTime *LocTime `gorm:"column:c_createtime" json:"CreateTime,omitempty"`
UpdateTime *LocTime `gorm:"column:c_updatetime" json:"UpdateTime,omitempty"`
// serializer:json; 告诉 GORM 在读写该字段时自动进行 JSON 转换
// 从数据库读取时 GORM 自动调用 json.Unmarshal → 反序列化到 interface{} 类型
// 写入数据库时 GORM 自动调用 json.Marshal → 存为文本: {"APIs":["/customer/info"]}
Permissions interface{} `gorm:"column:c_permissions;default:;serializer:json;" json:"Permissions,omitempty"`
}
# 关于 *gorm.DB
# gorm.DB.Statement
临时状态存储区,其结构体内部包含了构建一条完整 SQL 所需的所有上下文信息,当您写下 db.Where("name = ?", "Alice").Order("age desc") 时,这些条件并不是立即执行的,而是被逐步累积到当前 Statement 对象的 Clauses 中
# 其中包含三种数据模型
Model interface{} # 指向传入的模型对象,用于解析 Schema
Dest interface{} # 指向查询或更新操作的目标对象(接收体)
ReflectValue reflect.Value # Dest 的反射值缓存,用于提高性能
- tx.Statement.ReflectValue、tx.Statement.Dest、tx.Statement.Model 区别
tx.Statement.Dest
// 用户传入的变量,通常是指针(结构体指针、切片指针等)
// 在回调中,Dest 保持原始用户传入值,由传给 GORM 的实际参数类型决定。但在实践中,几乎总是指针
// 单个创建时类型是指针 *User,批量创建时类型是切片的指针 *[]User
db.Create(&user) // Dest = *User
db.Create(&users) // Dest = *[]User
tx.Statement.ReflectValue
// 内部可修改的反射对象
// GORM 内部对 Dest(或 Model)进行反射并自动解引用(去除指针) 后得到的
// 单个创建时,传入指针`*User`,`ReflectValue`就是`User`结构体的Value(User)
// 批量创建时,传入切片`[]User`,`ReflectValue`就是切片的Value([]User)
// 它是 GORM 底层用来遍历字段、赋值、读取 Tag 的实际操作对象
// 因为已经剥离了指针层,可以直接调用 FieldByName 修改字段
db.Statement.ReflectValue.FieldByName("UpdatedAt").Set(time.Now())
tx.Statement.Model
// 用于指定操作的模型类型,通常不包含实际要写入的业务数据
// 提供表名、关联关系、作用域等元数据,不用于直接读写数据,通常不包含实际要写入的业务数据
tx.Create(&user) // 普通创建时类型: `nil`
tx.Model(&User{}).Create(&user) // 显式指定 Model 类型: *User
# gorm.DB.WithContext
GORM v2 为数据库操作关联 context.Context 的核心方法,用于链路追踪、超时控制和请求取消等功能
- 基本用法:接收一个 context.Context 参数,并返回一个新的 *gorm.DB 实例,新实例中的所有数据库操作都会与传入的上下文绑定
// 创建一个带超时的 Context
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 将 ctx 注入 DB 实例,用于后续操作
var users []User
if err := db.WithContext(ctx).Where("age > ?", 18).Find(&users).Error; err != nil {
// 处理超时或其他错误
}
- 持续会话模式:对于事务等一系列连续的操作,可以将同一个带上下文的 *gorm.DB 实例保存下来,确保整个操作链都受同一个上下文的控制
// 开启事务
tx := db.WithContext(ctx).Begin()
if err := tx.Error; err != nil {
return err
}
// 使用带上下文的 tx 实例执行多个操作
if err := tx.Create(&User{Name: "Alice"}).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&Order{UserID: user.ID}).Error; err != nil {
tx.Rollback()
return err
}
tx.Commit()
- 超时控制与查询取消:防止慢查询拖垮服务的关键。如果请求在指定时间内未完成,ctx 会触发超时,并尝试取消对应的数据库操作
// 为查询设置一个3秒的超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var orders []Order
err := db.WithContext(ctx).Where("user_id = ?", userID).Find(&orders).Error
if errors.Is(err, context.DeadlineExceeded) {
// 处理超时逻辑
}
# CURD
# 增加、修改、删除
- 增加成功后会返回刚才增加的记录
func (con UserController) Add(c *gin.Context) {
user := models.User{
Username: "sylone",
Age: 18,
Email: "123@qq.com",
AddTime: int(time.Now().Unix()),
}
// 场景 1:普通创建(如果主键冲突会报错)
result := models.DB.Create(&user) // 通过数据的指针来创建
if result.RowsAffected > 1 {
fmt.Print(user.Id)
}
fmt.Println(result.RowsAffected)
fmt.Println(user.Id)
c.String(http.StatusOK, "add 成功")
// 场景 2:数据同步/幂等写入(如果主键冲突会不会报错)
err := models.DB.Create(user, func(tx *gorm.DB) *gorm.DB {
// 存在则更新,不存在则插入
// clause 包,允许开发者以结构化的方式构建复杂的 SQL 子句,而不是手写原生 SQL 字符串
// OnConflict 告诉数据库:如果插入的数据违反了主键或唯一索引,不要报错继续执行后续操作
// UpdateAll: true 将当前传入结构体中所有的非主键字段,全部更新到数据库的已有记录中
return tx.Clauses(clause.OnConflict{UpdateAll: true})
// 如果新数据与现有数据发生主键或唯一索引冲突,会忽略这条新数据,不报错也不更新原有数据
return tx.Clauses(clause.Insert{Modifier: "IGNORE"})
})
if err != nil {
log.Printf("sync user failed: %v", err)
}
}
- 修改
func (con UserController) Edit(c *gin.Context) {
user := models.User{Id: 7}
models.DB.Find(&user)
user.Username = "gin gorm" user.Age = 1
// Save 是“全量覆盖”:更新所有字段(包括零值),且如果主键为空会退化为插入(Create)
models.DB.Save(&user)
// Updates 是“增量更新”:默认只更新非零值字段,且绝对不会执行插入操作
models.DB.Update(&user)
c.String(http.StatusOK, "Edit")
// 为查询添加悲观锁(Pessimistic Locking),用于防止并发修改导致的数据冲突
// FOR UPDATE 会锁定这些行,在当前事务提交之前,其他事务无法修改这些行
// 用于"先查后改"场景——先读取数据,判断条件后再更新,确保读取和更新之间数据不会被别人改掉
models.DB.Clauses(clause.Locking{Strength: "UPDATE"}) // 只兼容 MySQL
}
- 删除
func (con UserController) Delete(c *gin.Context) {
user := models.User{Id: 8}
models.DB.Delete(&user)
c.String(http.StatusOK, "Delete")
}
- 批量删除
db.Where("email LIKE ?", "%jinzhu%").Delete(Email{})
// DELETE from emails where email LIKE "%jinzhu%";
db.Delete(Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";
func (con UserController) DeleteAll(c *gin.Context) {
user := models.User{}
models.DB.Where("id>9").Delete(&user)
c.String(http.StatusOK, "DeleteAll")
}
- 修改
func (con UserController) Edit(c *gin.Context) {
user := models.User{Id: 7}
models.DB.Find(&user)
user.Username = "gin gorm" user.Age = 1
models.DB.Save(&user)
c.String(http.StatusOK, "Edit")
}
- 原生sql
func (con UserController) Edit(c *gin.Context) {
exec := models.DB.Where("c_key = ?", allocKey).Updates(map[string]any{
// 使用 gorm.Expr 构建 SQL 表达式
// LAST_INSERT_ID(expr) 是 MySQL 函数,先计算括号内的表达式expr的值,并将计算结果返回
// 这里 expr 是当前 c_allocid 加上增量 inc
// 实现了原子自增并让后续 SELECT LAST_INSERT_ID() 能获取这个新值
"c_allocid": gorm.Expr("LAST_INSERT_ID(c_allocid + ?)", inc),
"c_updatetime": now.String(),
})
var ids []int64
// Raw 用于执行原生 SQL,.Pluck("id", &ids) 将查询结果的 id 列填充到 ids 切片中
if err = tx.Raw("select LAST_INSERT_ID() as id").Pluck("id", &ids).Error; err != nil {
return 0, err
}
}
# 查询语句
- 查找全部
func (con UserController) Index(c *gin.Context) {
user := []models.User{}
models.DB.Find(&user)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": user,
})
}
- 指定条件查找
user := []models.User{}
models.DB.Where("username=?", "王五").Find(&user)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": user,
})
var n = 5
nav := []models.Nav{}
models.DB.Where("id>?", n).Find(&nav)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": nav,
})
var n1 = 3
var n2 = 9
nav := []models.Nav{}
models.DB.Where("id > ? AND id < ?", n1, n2).Find(&nav)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": nav,
})
nav := []models.Nav{}
models.DB.Where("id in (?)", []int{3, 5, 6}).Find(&nav)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": nav,
})
nav := []models.Nav{}
models.DB.Where("title like ?", "%会%").Find(&nav)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": nav,
})
nav := []models.Nav{}
models.DB.Where("id between ? and ?", 3, 6).Find(&nav)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": nav,
})
- Or 条件
nav := []models.Nav{}
models.DB.Where("id=? OR id=?", 2, 3).Find(&nav)
nav := []models.Nav{}
models.DB.Where("id=?", 2).Or("id=?", 3).Or("id=4").Find(&nav)
- 选择字段查询
nav := []models.Nav{}
models.DB.Select("id, title,url").Find(&nav)
- 排序 Limit 、Offset
nav := []models.Nav{}
models.DB.Where("id>2").Order("id Asc").Find(&nav)
nav := []models.Nav{}
models.DB.Where("id>2").Order("sort Desc").Order("id Asc").Find(&nav)
nav := []models.Nav{}
odels.DB.Where("id>1").Limit(2).Find(&nav)
// 跳过 2 条查询 2 条
nav := []models.Nav{}
models.DB.Where("id>1").Offset(2).Limit(2).Find(&nav)
- 获取总数
nav := []models.Nav{}
var num int
models.DB.Where("id > ?", 2).Find(&nav).Count(&num)
- Distinct:从模型中选择不相同的值
nav := []models.Nav{}
models.DB.Distinct("title").Order("id desc").Find(&nav)
c.JSON(200, gin.H{ "nav": nav, })
- 将查询结果直接映射到指定的变量或结构体:Scan
# Find 需要基于模型(Model)操作,默认会映射到模型的全部字段
# Scan 更灵活,可以任意定义目标结构体,不依赖模型,适合复杂查询或非模型数据
// 原生 SQL 扫描到自定义结构体
type Result struct {
Name string
Age int
}
var result Result
db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result)
// 原生 SQL
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)
var result []models.User
models.DB.Raw("SELECT * FROM user").Scan(&result)
fmt.Println(result)
# 原生SQL
- 使用原生 sql 删除 user 表中的一条数据
result := models.DB.Exec("delete from user where id=?", 3)
fmt.Println(result.RowsAffected)
- 使用原生 sql 修改 user 表中的一条数据
result := models.DB.Exec("update user set username=? where id=2", "哈哈")
fmt.Println(result.RowsAffected)
- 查询 uid=2 的数据
var result models.User
models.DB.Raw("SELECT * FROM user WHERE id = ?", 2).Scan(&result)
fmt.Println(result)
- 查询 User 表中所有的数据
var result []models.User
models.DB.Raw("SELECT * FROM user").Scan(&result)
fmt.Println(result)
- 统计 user 表的数量
var count int
row := models.DB.Raw("SELECT count(1) FROM user").Row(&count )
row.Scan(&count)
# 关联查询
# 一对一
type Article struct {
Id int `json:"id"`
Title string `json:"title"`
Description int `json:"description"`
CateId string `json:"cate_id"` // 如果是ArticleCateId可以不用以下设置
State int `json:"state"`
// foreignkey 指定当前表的外键、references 指定关联表中和外键关联的字段
ArticleCate ArticleCate `gorm:"foreignKey:CateId;references:Id"`
}
type ArticleCate struct {
Id int `json:"id"`
Title string `json:"title"`
State int `json:"state"`
}
- 查询所有文章以及文章对应的分类信息
var articleList []models.Article
models.DB.Preload("ArticleCate").Limit(2).Find(&articleList)
c.JSON(200, gin.H{ "result": articleList, })
- 查询所有文章以及文章对应的分类信息 指定条件
var articleList []models.Article
models.DB.Preload("ArticleCate").Where("id>=?", 4).Find(&articleList)
c.JSON(200, gin.H{ "result": articleList, })
# 一对多
type ArticleCate struct {
Id int `json:"id"`
Title string `json:"title"`
State int `json:"state"`
Article []Article `gorm:"foreignKey:CateId"`
}
type Article struct {
Id int `json:"id"`
Title string `json:"title"`
Description int `json:"description"`
CateId string `json:"cate_id"`
State int `json:"state"`
}
- 查找所有分类以及分类下面的文章信息
var articleCateList []models.ArticleCate
models.DB.Preload("Article").Find(&articleCateList)
c.JSON(200, gin.H{ "result": articleCateList, })
- 查找所有分类以及分类下面的文章信息 指定条件
var articleCateList []models.ArticleCate
models.DB.Preload("Article").Where("id>0")Limit(1).Find(&articleCateList)
c.JSON(200, gin.H{ "result": articleCateList, })
# 多对多
type Lesson struct {
Id int `json:"id"`
Name string `json:"name"`
Student []*Student `gorm:"many2many:lesson_student"`
}
type Student struct {
Id int
Number string
Password string
ClassId int
Name string
Lesson []*Lesson `gorm:"many2many:lesson_student"`
}
type LessonStudent struct {
LessonId int // 建议这样命名,否则很麻烦
StudentId int // 建议这样命名,否则很麻烦
}
- 查询学生信息的时候获取学生的选课信息
studentList := []models.Student{}
models.DB.Preload("Lesson").Find(&studentList)
c.JSON(http.StatusOK, studentList)
- 查询张三选修了哪些课程
studentList := []models.Student{}
models.DB.Preload("Lesson").Where("id=1").Find(&studentList)
c.JSON(http.StatusOK, studentList)
- 课程被哪些学生选修了
lessonList := []models.Lesson{}
models.DB.Preload("Student").Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
- 计算机网络被那些学生选修了
lessonList := []models.Lesson{}
models.DB.Preload("Student").Where("id=1").Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
- 查询数据指定条件
lessonList := []models.Lesson{}
models.DB.Preload("Student").Offset(1).Limit(2).Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
# 关联查询指定子集的筛选条件
- 张三被开除了 查询课程被哪些学生选修的时候要去掉张三
lessonList := []models.Lesson{}
models.DB.Preload("Student", "id!=1").Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
lessonList := []models.Lesson{}
models.DB.Preload("Student", "id not in (1,2)").Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
# 自定义预加载 SQL
- 查看课程被哪些学生选修 要求:学生 id 倒叙输出
lessonList := []models.Lesson{}
models.DB.Preload("Student", func(db *gorm.DB) *gorm.DB {
return models.DB.Where("id>3").Order("id DESC")
}).Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
# 使用事务
- 事务(手动控制)
// 开启事务
tx := db.Begin()
// 在事务中做一些数据库操作 (这里应该使用 'tx' ,而不是 'db')
tx.Create(...)
// ... // 有错误时,手动调用事务的 Rollback()
tx.Rollback()
// 无错误时,手动调用事务的 Commit()
tx.Commit()
- 张三给李四转账
type TransitionController struct {
BaseController
}
func (con TransitionController) Index(c *gin.Context) {
tx := models.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
con.Error(c)
}
}()
if err := tx.Error; err != nil {
fmt.Println(err)
con.Error(c)
}
// 张三账户减去 100
u1 := models.Bank{Id: 1}
tx.Find(&u1)
u1.Balance = u1.Balance - 100
if err := tx.Save(&u1).Error; err != nil {
tx.Rollback()
con.Error(c)
}
// panic("遇到了错误")
// 李四账户增加 100
u2 := models.Bank{Id: 2}
tx.Find(&u2)
u2.Balance = u2.Balance + 100
// panic("失败")
if err := tx.Save(&u2).Error; err != nil {
tx.Rollback()
con.Error(c)
}
tx.Commit()
con.success(c)
}
# 注册回调
GORM 内部每个操作(Create/Update/Delete/Query)都有一条回调链(Callback),类似流水线按顺序执行
- 内置的回调链如下:
# Create 链: before_create → gorm:create → after_create
# Update 链: before_update → gorm:update → after_update
# Delete 链: before_delete → gorm:delete → after_delete
# Query 链: before_query → gorm:query → after_query
- 在 GORM 实际执行 SQL 之前插入自定义逻辑,在数据写入数据库前完成字段填充、权限校验等操作
func EnableHook(tx *gorm.DB) {
// tx.Callback().Create() — 获取 GORM 的 Create 回调链
//.Before("gorm:create") — 在 GORM 内置的 gorm:create 回调之前插入
//.Register("created_at", AutoCreateAt) — 注册名为created_at的回调,处理函数为AutoCreate
tx.Callback().Create().Before("gorm:create").Register("created_at", AutoCreate)
// 在 GORM 的 gorm:update 回调之前插入名为update_at的回调,处理函数为 AutoUpdate
tx.Callback().Update().Before("gorm:update").Register("update_at", AutoUpdate)
}
func AutoCreate(db *gorm.DB) {
if db.Error != nil {
return
}
// 获取 ReflectValue
rv := db.Statement.ReflectValue
// 确保 ReflectValue 是有效的
if !rv.IsValid() {
return
}
switch rv.Kind() {
case reflect.Struct:
// ================= 单个创建 =================
// rv 就是 User 结构体
// 获取 Name 字段并修改(例如加密)
nameField := rv.FieldByName("Name")
if nameField.IsValid() && nameField.CanSet() {
nameField.SetString("Encrypted_" + nameField.String())
}
case reflect.Slice, reflect.Array:
// ================= 批量创建 =================
// rv 是 []User 切片,需要遍历
for i := 0; i < rv.Len(); i++ {
// 获取切片中的每一个 User 结构体
elem := rv.Index(i)
nameField := elem.FieldByName("Name")
if nameField.IsValid() && nameField.CanSet() {
nameField.SetString("Encrypted_" + nameField.String())
}
}
}
}
← 基础知识