Gorm学习笔记 - 概述
资料地址:https://www.kancloud.cn/sliver_horn/gorm/1861152
概述
关联 (Has One、Has Many、Belongs To、Many To Many、多态、单表继承)
Create、Save、Update、Delete、Find 前/后的勾子
基于Preload、Joins的预加载
事务、嵌套事务、保存点、回滚至保存点
Context、Prepared Statment 模式、DryRun 模式
批量插入、FindInBatches、查询至 Map
SQL Builder, Upsert, Locking, Optimizer/Index/Comment Hints
复合主键
自动迁移
自定义 Logger
灵活的可扩展插件 API:Database Resolver(读写分离)、Prometheus
先把这些概念理解一遍,再对要点进行逐章学习记录
一、关联关系
在 ORM (对象关系映射) 中,关联关系是描述不同数据模型之间关系的核心概念。以下是各种关联关系的详细解释:
1. Has One (一对一)
表示一个模型拥有另一个模型,是一种一对一关系。
示例:
type User struct {gorm.ModelCreditCard CreditCard
}type CreditCard struct {gorm.ModelNumber stringUserID uint // 外键
}
特点:
一个用户只能拥有一张信用卡
外键存在于被拥有的模型中(CreditCard中的UserID)
2. Has Many (一对多)
表示一个模型拥有多个另一个模型的实例。
示例:
type User struct {gorm.ModelOrders []Order
}type Order struct {gorm.ModelUserID uint // 外键Amount float64
}
特点:
一个用户可以拥有多个订单
外键存在于被拥有的模型中(Order中的UserID)
3. Belongs To (属于)
表示一个模型属于另一个模型,是Has One/Has Many的反向关系。
示例:
type Order struct {gorm.ModelUser UserUserID uint // 外键
}
特点:
外键存在于当前模型中
表示从属关系,如订单属于用户
4. Many To Many (多对多)
表示两个模型之间可以相互拥有多个实例。
示例:
type User struct {gorm.ModelLanguages []Language `gorm:"many2many:user_languages;"`
}type Language struct {gorm.ModelName stringUsers []User `gorm:"many2many:user_languages;"`
}
特点:
需要一个中间表(user_languages)来存储关联关系
一个用户可以说多种语言,一种语言可以被多个用户使用
5. 多态关联
允许一个模型属于多个其他模型,通过一个接口实现。
示例:
type Comment struct {gorm.ModelContent stringCommentableID uintCommentableType string
}type Post struct {gorm.ModelTitle stringComments []Comment `gorm:"polymorphic:Commentable;"`
}type Video struct {gorm.ModelTitle stringComments []Comment `gorm:"polymorphic:Commentable;"`
}
特点:
评论可以属于帖子或视频
通过CommentableType和CommentableID字段确定关联
6. 单表继承
将多个模型存储在同一个数据库表中,通过类型字段区分。
示例:
type Product struct {gorm.ModelName stringType string `gorm:"type:varchar(20)"` // "Book", "Movie", etc.// Book specific fieldsAuthor stringPages int// Movie specific fieldsDirector stringDuration int
}
特点:
所有子类共享同一个表
通过Type字段区分不同类型
某些字段可能只对特定类型有意义
gorm在关联的时候,会有一些约定的字段写法,需要注意,这些并不是实际字段,这些字段后面,往往有一些修饰的gorm:的定义
二、Preload 和 Joins 的预加载机制
1. Preload 预加载
Preload是 GORM 中最常用的预加载方法,它通过额外的查询(通常是 IN 查询)来加载关联数据。
基本用法
// 查找用户并预加载其订单
db.Preload("Orders").Find(&users)// 预加载嵌套关联
db.Preload("Orders.OrderItems").Find(&users)// 带条件的预加载
db.Preload("Orders", "state = ?", "paid").Find(&users)
工作原理
首先执行主查询(查找 users)
然后执行额外的查询(查找与这些 users 关联的 orders)
最后将结果组装到相应的数据结构中
2. Joins 预加载
Joins预加载则使用 SQL JOIN 语句一次性获取主表和关联表的数据。
// 使用 JOIN 预加载
db.Joins("Company").Find(&users)// 带条件的 JOIN 预加载
db.Joins("Company", db.Where(&Company{Alive: true})).Find(&users)
工作原理
构建一个包含 JOIN 的 SQL 查询
一次性获取主表和关联表的数据
将结果映射到对应的数据结构
两种方式的比较
特性 | Preload | Joins |
---|---|---|
查询方式 | 多个独立查询 | 单个 JOIN 查询 |
性能 | 适合少量主记录+大量关联记录 | 适合大量主记录+少量关联记录 |
灵活性 | 可对每个关联单独设置条件 | 整个查询只能有一套条件 |
结果处理 | 自动组装嵌套结构 | 需要处理可能的重复数据 |
适用场景 | 复杂关联结构 | 简单关联且需要过滤或排序关联数据 |
三、事务管理机制
1. 事务 (Transaction)
事务是数据库操作的基本单位,它遵循ACID原则:
原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成
一致性(Consistency):事务执行前后,数据库从一个一致状态变为另一个一致状态
隔离性(Isolation):并发事务之间互不干扰
持久性(Durability):事务一旦提交,其结果就是永久性的
在ORM中的使用示例:
tx := db.Begin()// 在事务中执行操作
if err := tx.Create(&user).Error; err != nil {// 出错时回滚tx.Rollback()return err
}// 提交事务
tx.Commit()
2. 嵌套事务 (Nested Transaction)
嵌套事务是指在一个事务内部开启另一个事务,形成事务的层级结构。在大多数数据库中,实际上并不支持真正的嵌套事务,但ORM可以通过保存点(Savepoint)来模拟这种行为。
特点:
内层事务的提交不会真正提交到数据库,只有最外层事务的提交才会生效
内层事务的回滚只会回滚到该事务开始时的状态,不影响外层事务
外层事务的回滚会导致所有内层事务的操作都被回滚
示例:
tx1 := db.Begin()// 第一个操作
tx1.Create(&user1)// 嵌套事务开始
tx2 := tx1.Begin()
tx2.Create(&user2)
if someCondition {tx2.Rollback() // 只回滚tx2的操作
} else {tx2.Commit()
}// 提交最外层事务
tx1.Commit()
3. 保存点 (Savepoint) 、回滚至保存点 (Rollback to Savepoint)
保存点是事务中的一个标记点,可以用于部分回滚。它允许你回滚到事务中的某个特定点,而不是回滚整个事务。
特点:
可以在事务中设置多个保存点
可以回滚到指定的保存点,而不影响保存点之前的操作
保存点在事务提交或回滚后自动释放
示例:
tx := db.Begin()// 初始操作
tx.Create(&user1)// 设置保存点
tx.SavePoint("before_user2")// 尝试操作
tx.Create(&user2)
tx.Create(&user3)if someError {// 回滚到保存点,撤销user2和user3的创建tx.RollbackTo("before_user2")// 可以尝试其他操作tx.Create(&user4)
}// 提交事务
tx.Commit()
四、Context、Prepared Statement 模式 和 DryRun 模式
1. Context
作用:
允许在数据库操作中传递 context.Context,用于控制超时、取消操作或传递请求范围的元数据(如链路追踪的 trace_id)。
使用场景:
超时控制:避免数据库操作长时间阻塞。
取消操作:例如用户取消请求时,终止关联的数据库查询。
链路追踪:在微服务中传递上下文信息(如日志、监控)。
示例:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()// 将 ctx 传递给 GORM 操作
var user User
db.WithContext(ctx).First(&user, 1)
2. Prepared Statement 模式
作用:
启用后,GORM 会缓存预编译的 SQL 语句(Prepared Statements),提升重复查询的性能(特别是批量操作)。
原理:
首次执行 SQL 时,数据库会编译 SQL 并生成执行计划。
后续相同结构的 SQL 直接复用编译结果,减少解析开销。
使用场景:
高频执行的相同查询(如批量插入、循环更新)。
对性能敏感的场景。
示例:
// 全局启用(所有操作)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{PrepareStmt: true,
})// 或单次启用
tx := db.Session(&gorm.Session{PrepareStmt: true})
tx.First(&user, 1) // 此查询会复用预编译语句
3. DryRun 模式
作用:
只生成 SQL 但不实际执行,用于调试或验证 SQL 的正确性。
使用场景:
开发阶段检查 GORM 生成的 SQL 是否符合预期。
日志记录或审计 SQL 语句。
避免测试时污染数据库。
示例:
// 全局启用(所有操作)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{DryRun: true,
})// 或单次启用
tx := db.Session(&gorm.Session{DryRun: true})
tx.Create(&user) // 仅打印 SQL,不执行
// 输出:INSERT INTO users (...) VALUES (...)
获取生成的 SQL:
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement
sql := stmt.SQL.String() // 获取生成的 SQL
vars := stmt.Vars // 获取参数
三者的区别总结
特性 | 主要用途 | 适用场景 |
---|---|---|
Context | 控制超时、取消、传递元数据 | 超时控制、链路追踪、取消操作 |
Prepared Statement | 提升重复查询性能 | 高频相同查询(批量操作、循环) |
DryRun | 调试 SQL 不实际执行 | 开发调试、SQL 审计、测试避免污染 |
五、批量插入、FindInBatches、查询至 Map
1. 批量插入 (Batch Insert)
批量插入是指一次性将多条记录插入数据库,而不是逐条插入,这可以显著提高插入性能。
使用场景:
需要插入大量数据时
需要提高数据插入效率时
示例代码:
var users = []User{{Name: "Alice"}, {Name: "Bob"}, {Name: "Charlie"}}// 批量插入
result := db.Create(&users)// 分批插入(每批100条)
result := db.CreateInBatches(&users, 100)
优点:
减少数据库连接次数
提高插入速度
减少网络开销
2. FindInBatches
FindInBatches 用于分批查询大量数据,避免一次性加载所有数据到内存中。
使用场景:
处理大量数据时避免内存溢出
需要对大数据集进行批处理时
示例代码:
// 每批处理100条记录
db.Where("age > ?", 18).FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {for _, result := range results {// 处理每条记录}return nil
})
优点:
避免一次性加载大量数据导致内存不足
可以分批处理数据,适合大数据场景
可以在每批处理中添加业务逻辑
- 查询至 Map (Scan/Map)
查询至 Map 是指将查询结果直接映射到 map 或自定义结构体,而不是预定义的模型。
使用场景:
只需要查询部分字段时
查询结果不符合现有模型结构时
需要动态处理查询结果时
示例代码:
// 查询到 map
var result map[string]interface{}
db.Model(&User{}).First(&result)// 查询到自定义结构体
type UserDTO struct {Name stringAge int
}
var userDTO UserDTO
db.Model(&User{}).First(&userDTO)// 查询多行到 map 切片
var results []map[string]interface{}
db.Model(&User{}).Find(&results)
优点:
灵活性高,不依赖预定义模型
可以只查询需要的字段,减少数据传输量
适合动态查询和结果处理
总结对比
功能 | 主要用途 | 适用场景 | 性能考虑 |
---|---|---|---|
批量插入 | 高效插入多条数据 | 数据迁移、初始化数据 | 大幅提高插入性能 |
FindInBatches | 分批处理大量数据 | 大数据处理、批处理任务 | 避免内存溢出 |
查询至 Map | 灵活处理查询结果 | 动态查询、自定义结果处理 | 减少不必要的数据传输 |
六、SQL Builder、Upsert、 Locking、Optimizer/Index/Comment Hints
1.SQL Builder
GORM 提供了强大的 SQL 构建能力,允许你以链式调用的方式构建复杂的 SQL 查询。
// 基本查询构建
db.Where("name = ?", "jinzhu").Where("age >= ?", 18).Find(&users)// 复杂条件
db.Where("name <> ? AND age > ?", "jinzhu", 20).Find(&users)// 选择特定字段
db.Select("name", "age").Find(&users)// 排序
db.Order("age desc, name").Find(&users)// 分组
db.Model(&User{}).Select("name, sum(age) as total").Group("name").Find(&results)
2.Upsert
Upsert 是 “update or insert” 的缩写,用于在记录存在时更新,不存在时插入。
// 使用 Save 方法实现 Upsert
db.Save(&user) // 根据主键存在与否决定插入或更新// 使用 Clauses 实现更灵活的 Upsert
db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}}, // 冲突键DoUpdates: clause.AssignmentColumns([]string{"name", "age"}), // 更新哪些字段
}).Create(&user)// 使用 OnConflict 指定更新行为
db.Clauses(clause.OnConflict{UpdateAll: true, // 冲突时更新所有字段
}).Create(&users)
3.Locking
锁机制用于控制并发访问,GORM 支持行级锁。
// 悲观锁 - SELECT FOR UPDATE
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)// 共享锁 - SELECT FOR SHARE
db.Clauses(clause.Locking{Strength: "SHARE"}).Find(&users)// 指定锁模式
db.Clauses(clause.Locking{Strength: "UPDATE",Options: "NOWAIT", // 如果锁不可立即获取则返回错误
}).Find(&users)
4.Optimizer/Index/Comment Hints
这些提示用于影响查询优化器的行为。
索引提示
// 强制使用特定索引
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})// 忽略特定索引
db.Clauses(hints.IgnoreIndex("idx_user_name")).Find(&User{})// 强制索引连接顺序
db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_age").ForJoin()).Find(&User{})
优化器提示
// 设置优化器提示
db.Clauses(hints.New("MAX_EXECUTION_TIME(10000)")).Find(&User{}) // 设置最大执行时间10秒// MySQL 特定的优化器提示
db.Clauses(hints.New("/*+ MRR(user) */")).Find(&User{})
查询注释
// 添加查询注释
db.Clauses(hints.Comment("select from master")).Find(&User{})// 带条件的注释
db.Clauses(hints.CommentBefore("select", "from master")).Find(&User{})
综合示例
// 复杂查询示例
db.Clauses(hints.UseIndex("idx_age"),hints.Comment("获取活跃用户"),clause.Locking{Strength: "UPDATE"},
).Where("age > ?", 18).Order("last_active_at desc").Limit(100).Find(&activeUsers)// Upsert 并获取执行结果
result := db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "email"}},DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&user)// 检查受影响的行数
if result.RowsAffected == 0 {// 记录已存在并被更新
}
七、复合主键
复合主键是指由多个字段组合构成的主键,而不是仅由单个字段作为主键。在数据库设计中,当一个表需要由两个或多个字段共同唯一标识一条记录时,就会使用复合主键。
- 在GORM中,可以通过结构体标签 gorm:"primaryKey"为多个字段标记复合主键:
type Product struct {ID string `gorm:"primaryKey"`LanguageCode string `gorm:"primaryKey"`Name stringPrice float64
}
八、Database Resolver(读写分离)、Prometheus(插件)、灵活的可扩展插件 API
在GORM框架中,"灵活的可扩展插件API"指的是GORM提供了一套机制,允许开发者通过插件方式扩展框架功能。其中两个重要的插件示例是Database Resolver(用于读写分离)和Prometheus(用于监控)。
1. Database Resolver(读写分离)
Database Resolver插件主要用于实现数据库的读写分离功能:
读写分离:将读操作(SELECT)和写操作(INSERT/UPDATE/DELETE)路由到不同的数据库实例
多数据库支持:可以配置多个读库和写库
负载均衡:在读库之间进行负载均衡
故障转移:当某个数据库实例不可用时自动切换到其他实例
示例配置:
db.Use(dbresolver.Register(dbresolver.Config{Sources: []gorm.Dialector{mysql.Open("write_db_dsn")}, // 写库Replicas: []gorm.Dialector{mysql.Open("read_db_dsn")}, // 读库Policy: dbresolver.RandomPolicy{}, // 读库选择策略
}))
2. Prometheus 插件
Prometheus插件用于收集和暴露GORM的数据库操作指标:
监控指标:收集SQL查询数量、执行时间、错误率等
Prometheus集成:将收集的指标暴露给Prometheus监控系统
性能分析:帮助开发者识别慢查询和性能瓶颈
示例配置:
import "gorm.io/plugin/prometheus"db.Use(prometheus.New(prometheus.Config{DBName: "my_db", // 数据库名标识RefreshInterval: 15, // 指标刷新间隔(秒)PushAddr: "prometheus pusher address", // Prometheus推送地址StartServer: true, // 是否启动HTTP服务器暴露指标HTTPServerPort: 8080, // HTTP服务器端口
}))
3. 插件API的灵活性
GORM的插件API设计允许开发者:
轻松集成第三方插件
开发自定义插件扩展功能
通过钩子函数在数据库操作的生命周期中插入自定义逻辑
根据需求组合不同的插件
这种设计使得GORM能够保持核心简洁,同时通过插件机制满足各种复杂场景的需求。