GORM:Go 的 ORM 利器
在之前的文章中,我们学习了如何使用 database/sql 直接操作数据库。虽然这种方式灵活且性能高,但在实际项目中,大量的 CRUD 操作写起来非常繁琐,而且容易出错。
这时候,ORM(Object-Relational Mapping,对象关系映射)就派上用场了。ORM 把数据库的表映射成代码中的结构体,把 SQL 操作映射成方法调用,让数据库操作变得简单而安全。GORM 是 Go 语言中最流行的 ORM 框架,它功能强大、API 友好、性能优异。本文将从基础到进阶,全面讲解 GORM 的使用。
安装 GORM
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql # 或 sqlite, postgres, sqlserver
连接数据库
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:password@tcp(localhost:3306)/myapp?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("连接数据库失败")
}
sqlDB, _ := db.DB()
sqlDB.Ping()
println("数据库连接成功!")
}
定义模型
GORM 使用结构体来定义模型。通过标签(tags)可以配置表名、字段名、约束等。
package main
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string `gorm:"type:varchar(100);not null;comment:用户名"`
Email string `gorm:"uniqueIndex;not null;comment:邮箱"`
Age int `gorm:"default:0;comment:年龄"`
IsActive bool `gorm:"default:true;comment:是否激活"`
}
func (User) TableName() string {
return "users"
}
常用标签
| 标签 | 说明 | 例子 |
|---|---|---|
column | 列名 | gorm:"column:user_name" |
type | 数据类型 | gorm:"type:varchar(100)" |
size | 大小 | gorm:"size:255" |
primaryKey | 主键 | gorm:"primaryKey" |
unique | 唯一索引 | gorm:"unique" |
default | 默认值 | gorm:"default:0" |
not null | 非空 | gorm:"not null" |
autoIncrement | 自增 | gorm:"autoIncrement" |
comment | 注释 | gorm:"comment:用户邮箱" |
自动迁移
// 自动迁移(只会创建表、添加缺失的列和索引,不会删除或修改已有列)
db.AutoMigrate(&User{})
// 检查表是否存在
db.Migrator().HasTable(&User{})
// 删除表
db.Migrator().DropTable(&User{})
注意:AutoMigrate 只会在开发环境使用。生产环境应该使用专业的迁移工具(如 golang-migrate)。
CRUD 操作
Create(创建)
// 创建单条记录
user := User{Name: "张三", Email: "zhangsan@example.com", Age: 25}
db.Create(&user)
fmt.Println("用户ID:", user.ID)
// 批量创建
users := []User{
{Name: "李四", Email: "lisi@example.com", Age: 30},
{Name: "王五", Email: "wangwu@example.com", Age: 28},
}
db.Create(&users)
// 指定字段创建
db.Select("Name", "Email").Create(&user)
// 忽略某些字段
db.Omit("Age").Create(&user)
// 使用 Map 创建
db.Model(&User{}).Create(map[string]interface{}{
"Name": "赵六",
"Email": "zhaoliu@example.com",
"Age": 35,
})
Read(查询)
// 查询单条记录
var user User
db.First(&user)
db.Last(&user)
db.Take(&user)
db.First(&user, 1)
db.First(&user, "name = ?", "张三")
// 查询多条记录
var users []User
db.Find(&users)
db.Find(&users, []int{1, 2, 3})
db.Find(&users, "age > ?", 20)
// 条件查询
db.Where("name = ?", "张三").First(&user)
db.Where("name LIKE ?", "%张%").Find(&users)
db.Where("age BETWEEN ? AND ?", 20, 30).Find(&users)
// 链式查询
db.Where("age > ?", 20).
Where("is_active = ?", true).
Order("age desc").
Limit(10).
Offset(0).
Find(&users)
// 统计
var count int64
db.Model(&User{}).Where("age > ?", 20).Count(&count)
// Pluck
db.Model(&User{}).Pluck("name", &names)
Update(更新)
db.Model(&user).Update("name", "新名字")
db.Model(&user).Updates(User{Name: "新名字", Age: 27})
db.Model(&User{}).Where("age < ?", 20).Update("is_active", false)
db.Model(&User{}).Update("age", gorm.Expr("age + 1"))
Delete(删除)
db.Delete(&user)
db.Where("age < ?", 18).Delete(&User{})
db.Unscoped().Find(&users)
db.Unscoped().Delete(&user)
进阶概念:钩子机制
GORM 的钩子(Hooks)允许在特定事件前后执行自定义逻辑。完整的钩子执行顺序如下:创建时依次执行 BeforeCreate、BeforeSave、创建 SQL 操作、AfterCreate、AfterSave;更新时依次执行 BeforeUpdate、BeforeSave、更新 SQL 操作、AfterUpdate、AfterSave;删除时依次执行 BeforeDelete、删除 SQL 操作、AfterDelete;查询时依次执行 AfterFind(GORM v2 中 renamed 为 AfterFind)。
type Product struct {
ID uint
Slug string
Name string
Price float64
}
func (p *Product) BeforeCreate(tx *gorm.DB) error {
p.Slug = slugify(p.Name)
return nil
}
func (p *Product) AfterCreate(tx *gorm.DB) error {
log.Printf("商品 %s 已创建,价格 %.2f", p.Name, p.Price)
return nil
}
func (p *Product) BeforeUpdate(tx *gorm.DB) error {
if p.Price < 0 {
return errors.New("价格不能为负数")
}
return nil
}
func (p *Product) BeforeDelete(tx *gorm.DB) error {
log.Printf("商品 %s 被删除", p.Name)
return nil
}
func slugify(name string) string {
return strings.ToLower(strings.ReplaceAll(name, " ", "-"))
}
钩子中还可以使用 tx 参数进行额外的数据库操作,但要注意避免死循环(比如钩子里又触发同一个钩子)。
进阶概念:关联预加载策略
预加载(Preload)是解决 N+1 查询问题的核心手段。GORM 提供了三种预加载方式:Preload 自动查询关联数据、Joins 使用 JOIN 语句一次性查询、以及关联模式手动控制。
// 基础预加载
db.Preload("Posts").Find(&users)
// 预加载时添加条件
db.Preload("Posts", "status = ?", "published").Find(&users)
db.Preload("Posts", func(db *gorm.DB) *gorm.DB {
return db.Order("posts.created_at desc")
}).Find(&users)
// 多层预加载
db.Preload("Posts.Comments").Find(&users)
db.Preload("Posts.Comments.User").Find(&users)
// Joins 方式(一次性查询)
db.Joins("JOIN posts ON posts.user_id = users.id").
Where("posts.status = ?", "published").
Find(&users)
嵌套预加载与条件控制
db.Preload("Orders.Items.Product", func(db *gorm.DB) *gorm.DB {
return db.Select("id", "name", "price")
}).Find(&users)
使用 Preload 时,GORM 会执行主查询 + N 个关联查询。使用 Joins 时则只执行一个 JOIN 查询,但要注意 JOIN 会产生重复数据,适合一对一关联或计数统计。
进阶概念:自定义数据类型与复合主键
GORM 支持通过实现 Scanner 和 Valuer 接口来自定义数据类型。
type JSON map[string]interface{}
func (j JSON) Value() (driver.Value, error) {
if j == nil {
return nil, nil
}
return json.Marshal(j)
}
func (j *JSON) Scan(value interface{}) error {
if value == nil {
*j = nil
return nil
}
bytes, ok := value.([]byte)
if !ok {
return errors.New("type assertion to []byte failed")
}
return json.Unmarshal(bytes, j)
}
type User struct {
ID uint
Name string
Settings JSON `gorm:"type:json"`
}
复合主键
type OrderItem struct {
OrderID uint `gorm:"primaryKey"`
ProductID uint `gorm:"primaryKey"`
Quantity int
}
乐观锁
package main
import (
"errors"
"gorm.io/gorm"
)
type Inventory struct {
ID uint
ItemID uint `gorm:"uniqueIndex"`
Stock int
Version int `gorm:"default:1"`
}
func DeductStock(db *gorm.DB, itemID uint, quantity int) error {
for i := 0; i < 5; i++ {
var inv Inventory
if err := db.Where("item_id = ?", itemID).First(&inv).Error; err != nil {
return err
}
if inv.Stock < quantity {
return errors.New("库存不足")
}
result := db.Model(&Inventory{}).
Where("item_id = ? AND version = ?", itemID, inv.Version).
Updates(map[string]interface{}{
"stock": inv.Stock - quantity,
"version": inv.Version + 1,
})
if result.RowsAffected > 0 {
return nil
}
}
return errors.New("乐观锁重试失败")
}
进阶概念:SQL Builder 底层实现
GORM 底层的 SQL Builder 提供了强大的查询构造能力。通过 Session 配置可以控制全局行为。
// 使用 DryRun 模式预览 SQL
stmt := db.Session(&gorm.Session{DryRun: true}).
Model(&User{}).
Where("age > ?", 18).
Find(&[]User{}).Statement
fmt.Println(stmt.SQL.String())
// 使用命名参数
db.Where("name = @name AND age = @age", map[string]interface{}{
"name": "张三",
"age": 25,
}).Find(&users)
// 原生 SQL 查询
db.Raw("SELECT * FROM users WHERE age > ?", 18).Scan(&users)
// 原生 SQL 执行
db.Exec("UPDATE users SET age = age + 1 WHERE name = ?", "张三")
// 子查询
subQuery := db.Model(&Post{}).Select("user_id").Where("status = ?", "published")
db.Where("id IN (?)", subQuery).Find(&users)
ToSQL 方法可以获取生成的 SQL,方便调试和记录日志。
实战案例:完整的 CRUD 与搜索组合
package main
import (
"fmt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"index"`
Email string `gorm:"uniqueIndex"`
Age int
IsActive bool
}
func main() {
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
db.AutoMigrate(&User{})
// 批量创建测试数据
for i := 1; i <= 20; i++ {
db.Create(&User{Name: fmt.Sprintf("User%d", i), Email: fmt.Sprintf("u%d@test.com", i), Age: 20 + i, IsActive: i%2 == 0})
}
// 分页 + 排序 + 搜索组合
var users []User
var total int64
page, pageSize := 2, 5
keyword := "User1"
db.Model(&User{}).Where("name LIKE ?", "%"+keyword+"%").Count(&total)
db.Where("name LIKE ?", "%"+keyword+"%").Order("age desc").Limit(pageSize).Offset((page - 1) * pageSize).Find(&users)
fmt.Printf("总记录数: %d\n", total)
for _, u := range users {
fmt.Printf("ID=%d Name=%s Age=%d\n", u.ID, u.Name, u.Age)
}
}
实战案例:事务嵌套与读写分离
package main
import (
"errors"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/plugin/dbresolver"
)
func main() {
dsn := "user:pass@tcp(127.0.0.1:3306)/db?charset=utf8mb4&parseTime=True&loc=Local"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// 配置读写分离
db.Use(dbresolver.Register(dbresolver.Config{
Replicas: []gorm.Dialector{mysql.Open(dsn)},
}).SetConnMaxIdleTime(100))
// 事务嵌套
err := db.Transaction(func(tx *gorm.DB) error {
// 外层操作
if err := tx.Create(&User{Name: "外层用户"}).Error; err != nil {
return err
}
// 嵌套事务
if err := tx.Transaction(func(ntx *gorm.DB) error {
return ntx.Create(&Order{UserID: 1, Amount: 100}).Error
}); err != nil {
return err
}
return nil
})
if err != nil {
panic(err)
}
}
type Order struct {
ID uint
UserID uint
Amount float64
}
func performNestedTransaction(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{Name: "交易测试"}).Error; err != nil {
return err
}
if err := tx.Transaction(func(ntx *gorm.DB) error {
if err := ntx.Create(&Order{UserID: 1, Amount: 200}).Error; err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
})
}
性能对比:ORM 与原生方案
| 场景 | GORM | 手写 SQL | sqlx | ent |
|---|---|---|---|---|
| 简单查询 | 优秀(自动映射) | 快(无开销) | 优秀 | 优秀 |
| 复杂关联 | 良好(Preload) | 手动编写 | 良好 | 优秀 |
| 批量插入 | 良好(CreateInBatches) | 最快 | 良好 | 良好 |
| 类型安全 | 编译时检查 | 运行时检查 | 编译时(结构体) | 编译时生成 |
| 学习成本 | 中等 | 低 | 低 | 中等 |
对于简单查询,手写 SQL 的开销最小,GORM 大约慢 10% 到 20%。但对于复杂业务系统,GORM 的开发效率和维护性明显优于手写 SQL。ent 框架在类型安全方面表现最好,但学习曲线更陡峭。
最佳实践:模型定义与 N+1 避免
// 良好的模型命名和标签规范
type User struct {
ID uint `gorm:"column:id;primaryKey"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
Name string `gorm:"column:name;type:varchar(50);not null;index:idx_user_name"`
Email string `gorm:"column:email;type:varchar(100);uniqueIndex:uk_user_email"`
Age int `gorm:"column:age;default:0"`
IsActive bool `gorm:"column:is_active;default:true;comment:用户是否激活"`
Posts []Post `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
// 避免 N+1 查询的两种方式
// 方式一:Preload
db.Preload("Posts").Find(&users)
// 方式二:Joins + Preload 组合
db.Joins("Company").Preload("Orders").Find(&users)
最佳实践:连接池优化
package main
import (
"database/sql"
"log"
"time"
"gorm.io/gorm"
)
func configurePool(sqlDB *sql.DB) {
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
sqlDB.SetMaxIdleConns(10) // 空闲连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
sqlDB.SetConnMaxIdleTime(time.Minute * 10) // 空闲连接最大存活时间
// 定期打印连接池状态
sqlDB.Stats()
}
func logPoolStats(sqlDB *sql.DB) {
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
stats := sqlDB.Stats()
log.Printf("Pool: open=%d inUse=%d idle=%d wait=%d",
stats.MaxOpenConnections, stats.InUse, stats.Idle, stats.WaitCount)
}
}()
}
Scopes 与分页
func ActiveUsers(db *gorm.DB) *gorm.DB {
return db.Where("is_active = ?", true)
}
func OlderThan(age int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("age > ?", age)
}
}
type Pagination struct {
Page int
PageSize int
Total int64
}
func Paginate(p *Pagination) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if p.Page <= 0 { p.Page = 1 }
if p.PageSize <= 0 { p.PageSize = 10 }
if p.PageSize > 100 { p.PageSize = 100 }
offset := (p.Page - 1) * p.PageSize
return db.Offset(offset).Limit(p.PageSize)
}
}
// 使用
db.Scopes(ActiveUsers, OlderThan(20), Paginate(p)).Find(&users)
实战:完整的博客系统
package main
import (
"fmt"
"log"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"not null"`
Email string `gorm:"uniqueIndex;not null"`
Posts []Post
CreatedAt time.Time
}
type Post struct {
ID uint `gorm:"primarykey"`
UserID uint
User User
Title string `gorm:"not null"`
Content string `gorm:"type:text"`
Tags []Tag `gorm:"many2many:post_tags;"`
CreatedAt time.Time
UpdatedAt time.Time
}
type Tag struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"uniqueIndex;not null"`
Posts []Post `gorm:"many2many:post_tags;"`
}
func main() {
db, err := gorm.Open(sqlite.Open("blog.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(&User{}, &Post{}, &Tag{})
user := User{Name: "张三", Email: "zhangsan@example.com"}
db.Create(&user)
tag1 := Tag{Name: "Go"}
tag2 := Tag{Name: "教程"}
db.Create(&tag1)
db.Create(&tag2)
post := Post{
UserID: user.ID,
Title: "GORM 入门",
Content: "这是一篇关于 GORM 的教程...",
Tags: []Tag{tag1, tag2},
}
db.Create(&post)
var userWithPosts User
db.Preload("Posts.Tags").First(&userWithPosts, user.ID)
fmt.Printf("用户: %s\n", userWithPosts.Name)
for _, p := range userWithPosts.Posts {
fmt.Printf(" 文章: %s\n", p.Title)
for _, t := range p.Tags {
fmt.Printf(" 标签: %s\n", t.Name)
}
}
type UserPostCount struct {
Name string
PostCount int64
}
var counts []UserPostCount
db.Model(&User{}).
Select("users.name, count(posts.id) as post_count").
Joins("left join posts on posts.user_id = users.id").
Group("users.id").
Scan(&counts)
for _, c := range counts {
fmt.Printf("%s: %d 篇文章\n", c.Name, c.PostCount)
}
}
FAQ
Q1:GORM 的 First、Take 和 Find 有什么区别?
A:First 按主键升序排序后取第一条,找不到会返回 ErrRecordNotFound 错误。Take 不指定排序,直接取一条,不会出错只是返回空。Find 查询所有记录,也不返回错误。
Q2:为什么使用 Preload 后还是有 N+1 查询?
A:检查是否在循环中又访问了关联字段。Preload 只会自动加载顶层关联数据,如果在 for 循环里访问 p.Posts.Author,而 Author 没有预加载,仍然会产生 N+1。
Q3:软删除和物理删除如何使用?
A:默认 Delete 是软删除,会设置 deleted_at 字段。使用 db.Unscoped().Delete() 可以永久删除。在查询时加上 Unscoped 可以查到已删除记录。
Q4:AutoMigrate 可以用于生产环境吗?
A:不建议。AutoMigrate 只会添加缺失的列和索引,不会删除或修改已有列。生产环境推荐使用 golang-migrate 或 Flyway 等专业迁移工具。
Q5:如何实现批量插入并获取自增 ID?
A:使用 db.CreateInBatches(&users, 100) 分批插入,GORM 会自动回填主键到每个结构体的 ID 字段。
Q6:GORM 怎么记录执行的 SQL 日志?
A:通过配置 Logger。可以在 gorm.Config 中设置 Logger 的 LogLevel 为 Info 或更详细级别,也可以自定义 Logger 输出到文件。
Q7:复合主键的关联查询需要注意什么?
A:复合主键需要使用 belongs to 和 has many 等关联标签显式指定外键和引用字段,GORM 可能无法自动推断复合主键的关联关系。
Q8:GORM 连接池参数的最佳设置是什么?
A:MaxOpenConns 通常设为 CPU 核数的两倍到四倍,MaxIdleConns 与 MaxOpenConns 保持一致或略小,ConnMaxLifetime 设为不超过数据库的超时时间。
总结与延伸
本文全面介绍了 GORM 的基础用法和进阶特性。从安装连接到模型定义,从 CRUD 到钩子机制,从预加载到自定义数据类型,GORM 为 Go 开发者提供了强大的 ORM 能力。在性能敏感的场景中,合理使用 GORM 的底层 SQL Builder,结合连接池优化和预加载策略,可以在开发效率和运行性能之间取得平衡。
GORM 极大地简化了数据库操作,让你能专注于业务逻辑。但在复杂查询场景下,仍然可以结合原生 SQL 使用。更多高级用法请参考 GORM 官方文档和相关示例项目。掌握 GORM 的关键在于理解其底层的 SQL 生成原理,避免 N+1 查询,合理配置连接池,以及在合适时机使用原生 SQL。本文覆盖了日常开发中最常用和最重要的场景,配合示例代码可以帮助你快速建立起完整的 GORM 知识体系,在实际项目中游刃有余地进行数据库开发。
进阶概念
钩子机制(Hooks)
GORM 的钩子机制允许在特定数据库操作前后执行自定义逻辑,实现数据校验、自动填充、审计追踪等功能:
type User struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"type:varchar(100);not null"`
Slug string `gorm:"type:varchar(100);uniqueIndex"`
Password string `gorm:"type:varchar(255)"`
CreatedAt time.Time
UpdatedAt time.Time
}
// BeforeCreate:创建前生成 slug 和加密密码
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Slug == "" {
u.Slug = slugify(u.Name)
}
if u.Password != "" {
u.Password = hashPassword(u.Password)
}
return nil
}
// BeforeUpdate:更新前验证
func (u *User) BeforeUpdate(tx *gorm.DB) error {
if u.Name == "" {
return errors.New("用户名不能为空")
}
return nil
}
// AfterCreate:创建后发送通知
func (u *User) AfterCreate(tx *gorm.DB) error {
log.Printf("新用户 %s 注册成功", u.Name)
return nil
}
func slugify(name string) string {
re := regexp.MustCompile(`[^a-zA-Z0-9]+`)
return strings.ToLower(re.ReplaceAllString(name, "-"))
}
func hashPassword(pwd string) string {
hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
return string(hash)
}
GORM 支持的钩子有 Before/After Create、Before/After Update、Before/After Delete、Before/After Save、Before/After Find。
关联预加载
预加载避免了 N+1 查询问题。GORM 支持简单预加载和嵌套预加载:
// 简单预加载
db.Preload("Posts").Find(&users)
// 嵌套预加载
db.Preload("Posts.Tags").Preload("Profile").Find(&users)
// 条件预加载
db.Preload("Posts", "status = ?", "published").Find(&users)
// 自定义预加载查询
db.Preload("Posts", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at DESC").Limit(5)
}).Find(&users)
// Joins 预加载(使用 LEFT JOIN,避免多次查询)
db.Joins("Profile").Joins("Posts.Tags").Find(&users)
自定义数据类型
GORM 允许自定义字段类型,需实现 Scanner 和 Valuer 接口:
// 自定义 JSON 类型
type JSONMap map[string]interface{}
func (j JSONMap) Value() (driver.Value, error) {
return json.Marshal(j)
}
func (j *JSONMap) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New(fmt.Sprint("Failed to unmarshal JSON value:", value))
}
return json.Unmarshal(bytes, &j)
}
// 使用
type Config struct {
ID uint `gorm:"primarykey"`
Params JSONMap `gorm:"type:json"`
}
复合主键
type OrderItem struct {
OrderID uint `gorm:"primaryKey"`
ProductID uint `gorm:"primaryKey"`
Quantity int
Price float64
}
// 自动使用 OrderID + ProductID 作为复合主键
乐观锁
type Inventory struct {
ID uint `gorm:"primarykey"`
ProductID uint `gorm:"uniqueIndex"`
Quantity int
Version int `gorm:"default:1"`
}
func (i *Inventory) UpdateQuantity(tx *gorm.DB, delta int) error {
result := tx.Model(&Inventory{}).
Where("product_id = ? AND version = ?", i.ProductID, i.Version).
Updates(map[string]interface{}{
"quantity": gorm.Expr("quantity + ?", delta),
"version": gorm.Expr("version + 1"),
})
if result.RowsAffected == 0 {
return errors.New("乐观锁冲突,数据已被修改")
}
return nil
}
实战案例
1. 完整 CRUD
type ProductRepository struct {
db *gorm.DB
}
func (r *ProductRepository) Create(ctx context.Context, p *Product) error {
return r.db.WithContext(ctx).Create(p).Error
}
func (r *ProductRepository) GetByID(ctx context.Context, id uint) (*Product, error) {
var p Product
if err := r.db.WithContext(ctx).First(&p, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &p, nil
}
func (r *ProductRepository) Update(ctx context.Context, id uint, updates map[string]interface{}) error {
return r.db.WithContext(ctx).Model(&Product{}).Where("id = ?", id).Updates(updates).Error
}
func (r *ProductRepository) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&Product{}, id).Error
}
func (r *ProductRepository) List(ctx context.Context, filter ProductFilter) ([]Product, int64, error) {
query := r.db.WithContext(ctx).Model(&Product{})
if filter.CategoryID > 0 {
query = query.Where("category_id = ?", filter.CategoryID)
}
if filter.MinPrice > 0 {
query = query.Where("price >= ?", filter.MinPrice)
}
if filter.MaxPrice > 0 {
query = query.Where("price <= ?", filter.MaxPrice)
}
if filter.Keyword != "" {
query = query.Where("name LIKE ?", "%"+filter.Keyword+"%")
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var products []Product
err := query.Offset(filter.Offset()).Limit(filter.Limit()).
Order("created_at DESC").Find(&products).Error
return products, total, err
}
2. 分页排序搜索
type Pagination struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
Sort string `form:"sort"`
Order string `form:"order"`
}
func (p *Pagination) Offset() int {
if p.Page <= 0 {
p.Page = 1
}
return (p.Page - 1) * p.Limit()
}
func (p *Pagination) Limit() int {
if p.PageSize <= 0 {
p.PageSize = 20
}
if p.PageSize > 100 {
p.PageSize = 100
}
return p.PageSize
}
func WithPagination(query *gorm.DB, p *Pagination) *gorm.DB {
order := "created_at DESC"
if p.Sort != "" {
direction := "ASC"
if strings.ToUpper(p.Order) == "DESC" {
direction = "DESC"
}
order = fmt.Sprintf("%s %s", p.Sort, direction)
}
return query.Order(order).Offset(p.Offset()).Limit(p.Limit())
}
3. 事务嵌套
func (r *OrderRepository) CreateOrder(ctx context.Context, userID uint, items []CartItem) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 创建订单
order := Order{UserID: userID, Status: "pending"}
if err := tx.Create(&order).Error; err != nil {
return err
}
// 创建订单项
for _, item := range items {
orderItem := OrderItem{
OrderID: order.ID,
ProductID: item.ProductID,
Quantity: item.Quantity,
Price: item.Price,
}
if err := tx.Create(&orderItem).Error; err != nil {
return err
}
// 扣减库存
if err := tx.Model(&Product{}).Where("id = ?", item.ProductID).
UpdateColumn("stock", gorm.Expr("stock - ?", item.Quantity)).Error; err != nil {
return err
}
}
// 清空购物车(在嵌套事务中)
if err := tx.Transaction(func(innerTx *gorm.DB) error {
return innerTx.Where("user_id = ?", userID).Delete(&CartItem{}).Error
}); err != nil {
return err
}
return nil
})
}
4. 读写分离
type DBManager struct {
writeDB *gorm.DB
readDB *gorm.DB
}
func (m *DBManager) DB(ctx context.Context) *gorm.DB {
// 简单策略:GET 请求用读库
if method, ok := ctx.Value("http_method").(string); ok && method == "GET" {
return m.readDB.WithContext(ctx)
}
return m.writeDB.WithContext(ctx)
}
// 更精细的策略:支持 Hint 强制走主库
func (m *DBManager) ForceWrite(ctx context.Context) *gorm.DB {
return m.writeDB.WithContext(ctx)
}
func (m *DBManager) ForceRead(ctx context.Context) *gorm.DB {
return m.readDB.WithContext(ctx)
}
性能对比:GORM vs 手写 SQL vs sqlx vs ent
| 库 | 类型 | 开发效率 | 性能 | 学习曲线 | 适用场景 |
|---|---|---|---|---|---|
| GORM | ORM | 高 | 中 | 低 | 快速开发、CRUD 为主 |
| 手写 SQL | 原生 | 低 | 高 | 低 | 复杂查询、性能敏感 |
| sqlx | 轻量封装 | 中 | 高 | 低 | 需要扫描但不想要完整 ORM |
| ent | 代码生成 | 高 | 高 | 中 | GraphQL + 复杂关系 |
| bun | 现代 ORM | 高 | 高 | 低 | 追求性能和现代 API |
// GORM 批量插入(含蓄水层开销)
db.CreateInBatches(&users, 100)
// sqlx 批量插入(直接 SQL)
query := "INSERT INTO users (name, email) VALUES (:name, :email)"
_, err := sqlx.NamedExec(db, query, users)
// ent 批量插入(代码生成)
client.User.CreateBulk(
client.User.Create().SetName("张三").SetEmail("a@b.com"),
client.User.Create().SetName("李四").SetEmail("c@d.com"),
).Save(ctx)
FAQ
Q1:GORM 的 AutoMigrate 会删除数据吗?
A:不会。AutoMigrate 只添加缺失的列和索引,不会删除或修改已有列。生产环境建议使用 golang-migrate 等专业工具。
Q2:如何避免 GORM 的 N+1 查询?
A:使用 Preload 预加载关联数据,或使用 Joins 单查询完成。也可用 FindInBatches 批量查询。
Q3:GORM 怎么执行原生 SQL?
A:使用 db.Raw("SELECT * FROM users WHERE name = ?", name).Scan(&users) 或 db.Exec()。
Q4:Soft Delete 和 Hard Delete 怎么切换?
A:默认是软删除(更新 deleted_at 字段)。用 db.Unscoped().Delete(&user) 执行物理删除。
Q5:GORM 支持数据库迁移吗?
A:AutoMigrate 适合开发环境。生产环境推荐使用 golang-migrate 或 atlas 进行版本化迁移。
Q6:如何处理自定义表名前缀?
A:用 db.Table("my_prefix_users") 或全局配置 NamingStrategy{TablePrefix: "my_prefix_"}。
Q7:GORM 的 Association 和 Preload 有什么区别?
A:Preload 在查询时 JOIN 或二次查询加载关联数据;Association 用于对已加载模型的关联数据进行追加、替换、删除操作。
Q8:GORM Context 方法是线程安全的吗?
A:db.WithContext(ctx) 返回一个新的 *gorm.DB,可以安全地在多个 goroutine 中使用。
最佳实践
- 定义 Repository 层:封装数据访问逻辑,避免业务代码直接调用 GORM
- 使用 Context:所有查询都应传递 Context,支持超时和取消
- 避免 N+1 查询:用 Preload 或 Joins 预加载关联数据
- 合理使用钩子:复杂业务逻辑放 Service 层,钩子只用于简单校验和审计
- 事务控制:多个写操作必须在事务中执行
- 性能敏感用原生 SQL:复杂统计和报表查询可以手写 SQL
- 索引优化:根据查询模式在模型标签中声明索引
- 连接池配置:通过
db.DB()获取底层*sql.DB配置连接池
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。