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 // 其他数据库错误
}

AI 助手 - deepseek-v4-flash

你好!有什么可以帮你的吗?

可以问我:推荐文章、搜索主题、了解博客内容

AI 生成内容仅供参考

未播放
0:00 / 0:00