This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/DEV/GO/GORM.md
T

13 KiB
Raw Blame History

tags, create time
tags create time
DEV
GO
golang
GORM
backend
ORM
database
2026-04-22 10:00

GORM - Go 语言 ORM 框架

概述

GORM 是 Go 语言中最流行的 ORM(对象关系映射)框架,由 @jinzhu 创建。它基于 database/sql 构建,提供了链式 API、关联预加载、插件扩展等特性,让 Go 开发者以符合 Go 风格的方式高效操作数据库。

思考:为什么在有了 database/sql 标准库之后,还需要 GORM 这样的 ORM?ORM 的价值是什么?它的代价又是什么?

ORM 的核心价值在于将数据库表映射为结构体,用面向对象的方式操作数据,减少样板 SQL 代码。代价是:理解 ORM 生成的 SQL 至关重要,复杂查询场景下仍需手写 SQL。

安装与初始化

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql   # MySQL 驱动
go get -u gorm.io/driver/postgres # PostgreSQL 驱动
go get -u gorm.io/driver/sqlite   # SQLite 驱动
import (
    "gorm.io/gorm"
    "gorm.io/driver/mysql"
)

// MySQL 连接
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
    panic("failed to connect database")
}

定义模型

type User struct {
    ID        uint           `gorm:"primaryKey"`          // 主键,自动命名为 id
    Name      string         `gorm:"size:255;not null"`   // 非空 + 字段长度
    Email     string         `gorm:"uniqueIndex"`         // 唯一索引
    Age       int            `gorm:"default:0"`           // 默认值
    Role      string         `gorm:"default:'user'"`
    CreatedAt time.Time       // 自动管理 CreatedAt
    UpdatedAt time.Time       // 自动管理 UpdatedAt
    DeletedAt gorm.DeletedAt  // 软删除支持
}

GORM 约定:

约定 说明
表名 结构体名的复数形式(User → users),可通过 TableName() 重写
主键 名为 ID 的字段自动作为主键,类型推断为 INT UNIQUE PRIMARY KEY
字段名 Go 字段名 → snake_case 的列名(UserName → user_name)
CreatedAt/UpdatedAt 自动填充时间戳(可通过 DisableCreateTimeIndex / DisableUpdateTimeIndex 关闭)

CRUD 操作

创建

user := User{Name: "Alice", Age: 25, Role: "admin"}
result := db.Create(&user) // 返回 *gorm.Result

// 批量创建(一次 INSERT)
users := []User{
    {Name: "Bob", Age: 30},
    {Name: "Carol", Age: 28},
}
db.Create(&users) // GORM 自动优化为批量 INSERT

查询

// 单一查询
var user User
db.First(&user, 10)              // 主键查询
db.First(&user, "name = ?", "Alice") // 条件查询
db.Where("age > ?", 20).First(&user)  // Where 链式

// 多结果查询
var users []User
db.Where("role = ?", "user").Find(&users)

// 链式条件
db.Where("role = ?", "admin").
   Where("age >= ?", 25).
   Order("age desc").
   Limit(10).
   Offset(20).
   Find(&users)

GORM 查询 API 速查:

方法 等价 SQL
First / Take LIMIT 1,找第一条
Last ORDER BY PRIMARY KEY DESC LIMIT 1
Find SELECT * FROM ...
Where WHERE ...
Or OR ...
Not NOT ...
Order ORDER BY ...
Select SELECT col1, col2
Group / Having GROUP BY / HAVING
Joins JOIN ...
Scan 扫描到自定义结构体

注意:First、Last 在没有数据时返回 gorm.ErrRecordNotFound,务必处理此错误。

flowchart LR
    A["db.Model(&User{})"] --> B["Where 条件过滤"]
    B --> C["Order 排序"]
    C --> D["Select 字段选择"]
    D --> E["Limit/Offset 分页"]
    E --> F["Group/Having 分组"]
    F --> G["Preload 预加载关联"]
    G --> H["Find/First/Last 执行"]
    H --> I["Scan 扫描到结构体"]

    classDef start fill:#4285f4,color:#fff
    classDef step fill:#f1f3f4,stroke:#333
    classDef finish fill:#34a853,color:#fff
    class A start
    class I finish
    class B,C,D,E,F,G,H step

链式 API 的本质:每个方法返回 *gorm.DB 对象,不断累积查询条件,最终 Find/First 时才组装并执行 SQL。

更新

// 更新单个字段
db.Model(&user).Update("name", "Alice Updated")

// 更新多个字段
db.Model(&user).Updates(User{Name: "Alice V2", Age: 26, Role: "superadmin"})

// 带条件的更新
db.Where("age > ?", 18).Model(&user).Update("role", "adult")

// 用 map 更新(会跳过零值字段,慎用)
db.Model(&user).Updates(map[string]interface{}{"name": "New", "age": 30})

删除

// 软删除(推荐)
db.Delete(&user) // 设置 DeletedAt = now(),而非物理删除

// 硬删除
db.Unscoped().Delete(&user) // 物理删除

// 批量删除
db.Where("age < ?", 18).Delete(&User{})

关联(Associations)

GORM 支持三种关联关系,通过标签定义。

type User struct {
    gorm.Model
    Name      string
    Role      string
    CreditCards []CreditCard `gorm:"foreignKey:UserID"`   // HasMany
}

type CreditCard struct {
    gorm.Model
    Number   string
    UserID   uint `gorm:"not null"`             // 外键
}

三种关联类型

graph LR
    subgraph User 用户
        U1["ID\nName\nRole"]
    end

    subgraph CreditCard 信用卡
        C1["ID\nNumber\nUserID (FK)"]
    end

    subgraph Profile 个人资料
        P1["ID\nBio\nUserID (FK)"]
    end

    subgraph Language 语言[关联表: user_languages]
        L1["ID\nName"]
        UM["user_id"]
        LM["language_id"]
    end

    U1 -->|"1:N HasMany"| C1
    U1 -->|"1:1 HasOne"| P1
    U1 -.->|"N:M BelongsToMany"| L1

    classDef user fill:#4285f4,stroke:#336,stroke-width:2px,color:#fff
    classDef card fill:#ea4335,stroke:#c33,stroke-width:2px,color:#fff
    classDef profile fill:#34a853,stroke:#264,stroke-width:2px,color:#fff
    classDef lang fill:#fbbc04,stroke:#cc8,stroke-width:2px,color:#333

    class U1 user
    class C1 card
    class P1 profile
    class L1 lang

关键理解:标签写在一端,GORM 会根据外键位置自动判断关系方向。

  • HasOne:外键在对方模型(Profile.UserID)→ 写在 User 上用 hasOne
  • HasMany:外键在对方模型(CreditCard.UserID)→ 写在 User 上用 hasMany
  • BelongsTo:外键在当前模型 → 写在「从属」一方
  • BelongsToMany:需要中间关联表(user_languages)
关联类型 标签方式 示例
HasOne 外键在对方模型 Profile 包含 UserID,User 通过 HasOne 关联
HasMany 外键在对方模型 CreditCards 数组,CreditCard 包含 UserID
BelongsTo 外键在当前模型 CreditCard 包含 UserID,BelongsTo 指向 User
BelongsToMany 需要通过关联表 User ↔ Languages(多对多)

预加载(Preloading)

// N+1 问题:一次查询 N 条关联记录
db.Preload("CreditCards").Find(&users)
// 生成 2 条 SQL:SELECT * FROM users; SELECT * FROM credit_cards WHERE user_id IN (...)

// 嵌套预加载
db.Preload("CreditCards").
   Preload("Profile").
   Find(&users)

// 带条件的预加载
db.Preload("CreditCards", "type = ?", "premium").Find(&users)

// 使用 Joins 预加载(生成 INNER JOIN,减少查询次数)
db.Joins("Profile").Find(&users)

核心原则:批量查询时务必使用 Preload,否则会在循环中触发 N+1 查询问题。

flowchart LR
    subgraph Bad ["❌ 不使用 Preload - N+1 问题"]
        direction TB
        S1["SQL 1: SELECT * FROM users"] --> Loop["循环 N 次"]
        Loop --> S2["SQL 2: SELECT * FROM credit_cards WHERE user_id=1"]
        Loop --> S3["SQL 3: SELECT * FROM credit_cards WHERE user_id=2"]
        Loop --> S4["SQL N: SELECT * FROM credit_cards WHERE user_id=N"]
    end

    subgraph Good ["✅ 使用 Preload - 2 条 SQL"]
        direction TB
        G1["SQL 1: SELECT * FROM users"] --> G2["SQL 2: SELECT * FROM credit_cards WHERE user_id IN (1,2,...,N)"]
    end

多对多关联(BelongsToMany)

type User struct {
    gorm.Model
    Name       string
    Languages  []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
    gorm.Model
    Name string
}

// 自动创建关联表 user_languages(字段:user_id, language_id)
// 插入关联数据
db.Model(&user).Association("Languages").Append(&Language{Name: "Go"})

// 查询关联
db.Preload("Languages").Find(&users)

// 替换关联
db.Model(&user).Association("Languages").Replace(
    &Language{Name: "Python"}, &Language{Name: "Rust"},
)

高级查询技巧

Raw SQL & 条件构造

// 原生 SQL
db.Raw("SELECT name, SUM(amount) FROM orders WHERE status = ? GROUP BY name", "PAID").
   Scan(&results)

// 条件表达式
db.Where(
    "name LIKE ? AND age BETWEEN ? AND ?", "%jon%", 20, 30,
).Find(&users)

// IN 查询
db.Where("id IN ?", []int{1, 2, 3}).Find(&users)

// 子查询
subQuery := db.Model(&Order).Select("user_id").Where("amount > ?", 100)
db.Where("id IN (?)", subQuery).Find(&users)

事务

tx := db.Begin() // 注意:事务中需使用 tx 而非 db

defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
    }
}()

if err := tx.Create(&user).Error; err != nil {
    tx.Rollback()
    return err
}

if err := tx.Create(&order).Error; err != nil {
    tx.Rollback()
    return err
}

return tx.Commit().Error

关键点:事务内的所有操作必须使用事务句柄 tx,而非全局 db。

自定义表名与列名

func (User) TableName() string {
    return "app_users" // 不使用复数形式
}

// 列名自定义
type User struct {
    ID    int    `gorm:"column:uid"`
    Name  string `gorm:"column:username"`
}

Scope(可复用查询片段)

func AmountGT(amount int) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        return db.Where("amount > ?", amount)
    }
}

func Active() func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        return db.Where("status = ?", "active")
    }
}

// 使用
db.Scopes(AmountGT(100), Active()).Find(&orders)

性能优化

1. Select 指定字段

// 只查询需要的字段,避免加载大字段(如 text/blob)
db.Select("name", "age").Find(&users)

2. 批量操作

// 批量写入:1次 INSERT N行,而非 N次 INSERT
db.Create(&users)

// 批量更新:1次 UPDATE
db.Model(&user).Where("id IN ?", ids).Update("status", "active")

// 批量删除
db.Delete(&User{}, ids)

3. 关闭预编译(极端性能场景)

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    PrepareStmt: true, // 开启预编译,适合高并发同一 SQL
})

开启 PrepareStmt 后,GORM 会使用 sql.DB.Prepare + sql.Stmt 缓存预编译语句,适合大量相同 SQL 的执行场景。

企业级注意事项

🚫 严禁使用 AutoMigrate

在团队协作和企业级项目中,禁止使用 db.AutoMigrate()。原因:

  1. 不可控的 DDL:自动建表/改表可能导致生产数据丢失
  2. DBA 审计要求:所有表结构变更需经过 SQL 审查
  3. 版本管理:数据库结构应通过迁移脚本(如 golang-migrate)管理

推荐做法:通过 SQL 脚本初始化数据库,与 数据库设计 文档保持一致。

事务与并发

// ❌ 错误:多个 goroutine 共享同一个 db 连接
// db.Create(&user1)  // 并发不安全

// ✅ 正确:事务自带连接池,每个 goroutine 开启独立事务
func saveUser(tx *gorm.DB, user *User) error {
    return tx.Create(user).Error
}

GORM 底层使用 sql.DB 连接池,本身是并发安全的,但 db.Begin() 返回的事务对象不可跨 goroutine 共享。

错误处理

result := db.Where("id = ?", id).First(&user)
if result.Error != nil {
    if errors.Is(result.Error, gorm.ErrRecordNotFound) {
        // 记录不存在
    } else {
        // 其他数据库错误
        return result.Error
    }
}

GORM vs 原生 SQL

场景 推荐方案 原因
简单 CRUD GORM 代码简洁,开发效率高
复杂 JOIN GORM Joins GORM 链式写法比手写 SQL 更直观
聚合查询(GROUP BY + HAVING) GORM Select/Group/Having 原生 SQL 也可,看团队偏好
复杂子查询/CTE 原生 SQL Raw/Scan GORM 对复杂 SQL 支持有限
高性能写入(大批量) GORM 批量 或直接用 sql.Batch
数据库迁移 SQL 脚本 / golang-migrate GORM AutoMigrate 禁用于生产

关联笔记