Golang - 6.Gorm
GORM 详解
基于 GORM v2,配合 PostgreSQL 使用
目录
1. 连接与配置
基本连接
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
dsn := "host=localhost user=postgres password=postgres dbname=gin_blog_development port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
常用配置项
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
// 跳过默认事务(提升约 30% 写入性能)
SkipDefaultTransaction: true,
// 命名策略
NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_", // 表名前缀,User → t_users
SingularTable: true, // 使用单数表名,User → t_user(而非 t_users)
},
// 日志
Logger: logger.New(logWriter, logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Warn,
IgnoreRecordNotFoundError: true, // 忽略 ErrRecordNotFound 日志
Colorful: true,
}),
// 禁用外键约束
DisableForeignKeyConstraintWhenMigrating: true,
})
连接池配置
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间
2. 模型定义
基础模型
type User struct {
ID uint `gorm:"primarykey" json:"id"`
Username string `gorm:"type:varchar(50);uniqueIndex;not null" json:"username"`
Password string `gorm:"type:varchar(255);not null" json:"-"`
Email string `gorm:"type:varchar(100);uniqueIndex" json:"email"`
Age int `gorm:"default:18" json:"age"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 软删除
}
gorm 标签速查
| 标签 | 说明 | 示例 |
|---|---|---|
column |
指定列名 | column:user_name |
type |
列类型 | type:varchar(100) |
primarykey |
主键 | primarykey |
autoIncrement |
自增 | autoIncrement |
default |
默认值 | default:18 |
not null |
非空 | not null |
uniqueIndex |
唯一索引 | uniqueIndex |
index |
普通索引 | index |
size |
字段大小 | size:255 |
precision |
精度 | precision:10 |
- |
忽略该字段 | gorm:"-" |
-:all |
忽略读写 | gorm:"-:all" |
-:migration |
忽略迁移 | gorm:"-:migration" |
embedded |
嵌入结构体 | embedded |
自定义表名
func (User) TableName() string {
return "users"
}
嵌入结构体
type BaseModel struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
type User struct {
BaseModel // 嵌入,字段提升到 User 层级
Username string `gorm:"type:varchar(50);not null" json:"username"`
}
3. CRUD 操作
Create(创建)
// 创建单条记录
user := User{Username: "sui", Email: "sui@example.com"}
result := db.Create(&user)
result.Error // 错误
result.RowsAffected // 影响行数
user.ID // 自动回填 ID
// 创建多条记录
users := []User{{Username: "a"}, {Username: "b"}}
db.Create(&users)
// 指定字段创建
db.Select("Username", "Email").Create(&user)
// INSERT INTO users (username, email) VALUES ("sui", "sui@example.com")
// 排除字段创建
db.Omit("Email").Create(&user)
// INSERT INTO users (username) VALUES ("sui")
Read(查询)
// 单条查询
var user User
db.First(&user) // 按主键升序第一条
db.Last(&user) // 按主键降序第一条
db.Take(&user) // 不排序取一条
// 按主键查询
db.First(&user, 1) // SELECT * FROM users WHERE id = 1
db.First(&user, "id = ?", 1) // 同上
// 检查记录是否存在
err := db.First(&user, 1).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
// 记录不存在
}
// 查询全部
var users []User
db.Find(&users)
// 条件查询
db.Where("username = ?", "sui").First(&user)
db.Where("age > ?", 18).Find(&users)
db.Where("username IN ?", []string{"sui", "admin"}).Find(&users)
db.Where("username LIKE ?", "%sui%").Find(&users)
db.Where("age BETWEEN ? AND ?", 18, 30).Find(&users)
// Struct 条件(零值字段会被忽略!)
db.Where(&User{Username: "sui"}).Find(&users)
// SELECT * FROM users WHERE username = "sui"
db.Where(&User{Username: "sui", Age: 0}).Find(&users)
// SELECT * FROM users WHERE username = "sui" ← Age:0 被忽略!
// Map 条件(零值不会被忽略)
db.Where(map[string]interface{}{"Username": "sui", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE username = "sui" AND age = 0
// Or 条件
db.Where("age > ?", 18).Or("username = ?", "sui").Find(&users)
// Not 条件
db.Not("username = ?", "sui").Find(&users)
Update(更新)
// 保存全部字段(零值也会更新)
db.Save(&user) // UPDATE users SET username="sui", age=0, ... WHERE id=1
// 更新单个字段
db.Model(&user).Update("username", "new_name")
db.Model(&User{}).Where("age > ?", 18).Update("age", 20)
// 更新多个字段(struct,零值字段会被忽略)
db.Model(&user).Updates(User{Username: "new", Age: 20})
// UPDATE users SET username="new", age=20 WHERE id=1
// 更新多个字段(map,零值不会被忽略)
db.Model(&user).Updates(map[string]interface{}{"Age": 0, "Username": "new"})
// Select 指定更新字段
db.Model(&user).Select("Username").Updates(User{Username: "new", Age: 0})
// UPDATE users SET username="new" WHERE id=1 ← Age 被排除
// 表达式更新
db.Model(&user).Update("age", gorm.Expr("age + ?", 1))
// UPDATE users SET age = age + 1 WHERE id = 1
Delete(删除)
// 按主键删除
db.Delete(&user, 1)
db.Delete(&User{}, 1)
// 条件删除
db.Where("age < ?", 18).Delete(&User{})
// 批量删除
db.Where("age < ?", 18).Delete(&User{})
// 软删除(模型包含 DeletedAt 字段时自动启用)
db.Delete(&user) // UPDATE users SET deleted_at=NOW() WHERE id=1
// 查询包含软删除的记录
db.Unscoped().Find(&users)
// 永久删除(跳过软删除)
db.Unscoped().Delete(&user)
4. 查询构建器
链式方法
db.Select("id", "username").
Where("age > ?", 18).
Order("created_at desc").
Limit(10).
Offset(20).
Find(&users)
// SELECT id, username FROM users WHERE age > 18 ORDER BY created_at DESC LIMIT 10 OFFSET 20
分页封装
func Paginate(db *gorm.DB, page, pageSize int) *gorm.DB {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
// 使用
db.Scopes(Paginate(page, pageSize)).Find(&users)
子查询
// WHERE age > (SELECT AVG(age) FROM users)
subQuery := db.Model(&User{}).Select("AVG(age)")
db.Where("age > (?)", subQuery).Find(&users)
Group & Having
type Result struct {
Age int
Count int
}
db.Model(&User{}).
Select("age, count(*) as count").
Group("age").
Having("count > ?", 1).
Find(&results)
// SELECT age, count(*) as count FROM users GROUP BY age HAVING count(*) > 1
Join
db.Table("users").
Select("users.id, orders.order_no").
Joins("LEFT JOIN orders ON orders.user_id = users.id").
Find(&results)
原生 SQL
// 原生查询
db.Raw("SELECT id, username FROM users WHERE age > ?", 18).Scan(&results)
// 原生执行
db.Exec("UPDATE users SET age = ? WHERE id = ?", 20, 1)
5. 关联关系
Belongs To(多对一)
type User struct {
ID uint `json:"id"`
Name string `json:"name"`
}
type Article struct {
ID uint `json:"id"`
Title string `json:"title"`
UserID uint `json:"user_id"`
User User `gorm:"foreignKey:UserID" json:"user"` // 外键指向 User
}
Has One(一对一)
type User struct {
ID uint `json:"id"`
Profile Profile `gorm:"foreignKey:UserID" json:"profile"`
}
type Profile struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Bio string `json:"bio"`
}
Has Many(一对多)
type User struct {
ID uint `json:"id"`
Articles []Article `gorm:"foreignKey:UserID" json:"articles"`
}
type Article struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Title string `json:"title"`
}
Many To Many(多对多)
type Article struct {
ID uint `json:"id"`
Title string `json:"title"`
Tags []Tag `gorm:"many2many:article_tags;" json:"tags"`
}
type Tag struct {
ID uint `json:"id"`
Name string `json:"name"`
}
// 自动创建 article_tags 关联表
预加载(Eager Loading)
// 加载关联
db.Preload("Articles").Find(&users)
// SELECT * FROM users;
// SELECT * FROM articles WHERE user_id IN (1,2,3);
// 嵌套预加载
db.Preload("Articles.Tags").Find(&users)
// 条件预加载
db.Preload("Articles", "status = ?", "published").Find(&users)
// Join 预加载(单查询,适合过滤)
db.Joins("Articles").Find(&users)
关联操作
// 添加关联
db.Model(&article).Association("Tags").Append([]Tag{{Name: "go"}, {Name: "gin"}})
// 替换关联
db.Model(&article).Association("Tags").Replace([]Tag{{Name: "go"}})
// 删除关联(只删关联表记录,不删模型)
db.Model(&article).Association("Tags").Delete(tag)
// 清空关联
db.Model(&article).Association("Tags").Clear()
// 统计关联数
db.Model(&article).Association("Tags").Count()
6. 事务
自动事务(默认行为)
// 默认每次 Create/Update/Delete 都包裹在事务中
// 可通过 SkipDefaultTransaction: true 关闭以提升性能
手动事务
// 方式1:闭包(推荐)
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{Username: "a"}).Error; err != nil {
return err // 返回 error 自动回滚
}
if err := tx.Create(&Article{Title: "test"}).Error; err != nil {
return err
}
return nil // 返回 nil 自动提交
})
// 方式2:手动控制
tx := db.Begin()
if err := tx.Create(&User{Username: "a"}).Error; err != nil {
tx.Rollback()
return err
}
tx.Commit()
嵌套事务(SavePoint)
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&User{Username: "a"})
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&User{Username: "b"})
return errors.New("rollback inner") // 只回滚内层
})
tx.Create(&User{Username: "c"})
return nil // 提交外层
})
// 结果:a 和 c 被创建,b 被回滚
7. 钩子(Hooks)
模型生命周期回调,按固定顺序执行:
type User struct {
// ...
}
// Creating
func (u *User) BeforeCreate(tx *gorm.DB) error {
u.Password = hashPassword(u.Password) // 创建前加密密码
return nil
}
func (u *User) AfterCreate(tx *gorm.DB) error {
log.Printf("user %d created", u.ID)
return nil
}
// Updating
func (u *User) BeforeUpdate(tx *gorm.DB) error {}
func (u *User) AfterUpdate(tx *gorm.DB) error {}
// Deleting
func (u *User) BeforeDelete(tx *gorm.DB) error {}
func (u *User) AfterDelete(tx *gorm.DB) error {}
// Finding
func (u *User) AfterFind(tx *gorm.DB) error {
u.Password = "" // 查询后清除密码
return nil
}
跳过钩子:
db.Session(&gorm.Session{SkipHooks: true}).Create(&user)
8. 作用域(Scopes)
可复用的查询条件:
// 定义
func AgeGreaterThan(age int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("age > ?", age)
}
}
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
func OrderByCreatedAtDesc(db *gorm.DB) *gorm.DB {
return db.Order("created_at desc")
}
// 使用(可组合)
db.Scopes(
AgeGreaterThan(18),
Paginate(1, 10),
OrderByCreatedAtDesc,
).Find(&users)
// SELECT * FROM users WHERE age > 18 ORDER BY created_at DESC LIMIT 10 OFFSET 0
9. 迁移
AutoMigrate
// 自动创建表、添加缺失的列和索引
// 不会删除列、不会修改列类型
db.AutoMigrate(&User{}, &Article{})
// 禁用自动迁移中的外键约束
db.Set("gorm:disable_foreign_key_constraint_when_migrating", true).AutoMigrate(&User{})
手动迁移
// 检查表是否存在
db.Migrator().HasTable(&User{})
db.Migrator().HasTable("users")
// 创建表
db.Migrator().CreateTable(&User{})
// 删除表
db.Migrator().DropTable(&User{})
// 添加列
db.Migrator().AddColumn(&User{}, "Nickname")
// 删除列
db.Migrator().DropColumn(&User{}, "Nickname")
// 添加索引
db.Migrator().CreateIndex(&User{}, "Username")
// 检查索引是否存在
db.Migrator().HasIndex(&User{}, "Username")
10. 常见陷阱与最佳实践
⚠️ Struct 条件零值被忽略
// ❌ 错误:Age=0 不会出现在 WHERE 中
db.Where(&User{Username: "sui", Age: 0}).Find(&users)
// SELECT * FROM users WHERE username = 'sui'
// ✅ 正确:用 map 或显式条件
db.Where(map[string]interface{}{"username": "sui", "age": 0}).Find(&users)
db.Where("username = ? AND age = ?", "sui", 0).Find(&users)
⚠️ Updates 零值被忽略
user.Age = 0
// ❌ 错误:Age=0 不会被更新
db.Model(&user).Updates(user)
// ✅ 正确
db.Model(&user).Updates(map[string]interface{}{"age": 0})
db.Model(&user).Select("age").Updates(user) // 显式指定
⚠️ Save 会更新所有字段
// ❌ 危险:会把所有字段都 UPDATE,包括零值
db.Save(&user)
// ✅ 安全:只更新指定字段
db.Model(&user).Select("username", "age").Updates(user)
✅ 使用 SkipDefaultTransaction 提升性能
gorm.Open(postgres.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true, // 写入性能提升约 30%
})
需要事务时手动 db.Transaction(...) 即可。
✅ 合理使用 Session
// 共享条件的新 session
tx := db.Where("age > ?", 18).Session(&gorm.Session{})
var adults []User
tx.Find(&adults) // 复用 WHERE age > 18
var count int64
tx.Model(&User{}).Count(&count) // 复用 WHERE age > 18
✅ DryRun 调试 SQL
stmt := db.Session(&gorm.Session{DryRun: true}).
Where("age > ?", 18).
Find(&users).
Statement
fmt.Println(stmt.SQL.String()) // 打印生成的 SQL
✅ 错误处理模式
// DAO 层:直接返回 error
func (d *UserDao) GetByID(id uint) (*model.User, error) {
var user model.User
if err := d.db.First(&user, id).Error; err != nil {
return nil, err // 让调用方区分 ErrRecordNotFound 和其他错误
}
return &user, nil
}
// Service 层:区分业务错误
user, err := d.dao.GetByID(id)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound // 转为业务错误
}
if err != nil {
return nil, err // 其他数据库错误
}