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

471 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [DEV, GO, golang, GORM, backend, ORM, database]
create time: 2026-04-22 10:00
---
# GORM - Go 语言 ORM 框架
## 概述
GORM 是 Go 语言中最流行的 ORM(对象关系映射)框架,由 [@jinzhu](https://github.com/jinzhu) 创建。它基于 `database/sql` 构建,提供了链式 API、关联预加载、插件扩展等特性,让 Go 开发者以符合 Go 风格的方式高效操作数据库。
> **思考**:为什么在有了 `database/sql` 标准库之后,还需要 GORM 这样的 ORM?ORM 的价值是什么?它的代价又是什么?
ORM 的核心价值在于**将数据库表映射为结构体**,用面向对象的方式操作数据,减少样板 SQL 代码。代价是:理解 ORM 生成的 SQL 至关重要,复杂查询场景下仍需手写 SQL。
## 安装与初始化
```bash
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 驱动
```
```go
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")
}
```
## 定义模型
```go
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 操作
### 创建
```go
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
```
### 查询
```go
// 单一查询
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`,务必处理此错误。
```mermaid
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。
### 更新
```go
// 更新单个字段
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})
```
### 删除
```go
// 软删除(推荐)
db.Delete(&user) // 设置 DeletedAt = now(),而非物理删除
// 硬删除
db.Unscoped().Delete(&user) // 物理删除
// 批量删除
db.Where("age < ?", 18).Delete(&User{})
```
## 关联(Associations)
GORM 支持三种关联关系,通过标签定义。
```go
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"` // 外键
}
```
### 三种关联类型
```mermaid
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)
```go
// 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 查询问题。
```mermaid
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)
```go
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 & 条件构造
```go
// 原生 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)
```
### 事务
```go
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`。
### 自定义表名与列名
```go
func (User) TableName() string {
return "app_users" // 不使用复数形式
}
// 列名自定义
type User struct {
ID int `gorm:"column:uid"`
Name string `gorm:"column:username"`
}
```
### Scope(可复用查询片段)
```go
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 指定字段
```go
// 只查询需要的字段,避免加载大字段(如 text/blob)
db.Select("name", "age").Find(&users)
```
### 2. 批量操作
```go
// 批量写入: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. 关闭预编译(极端性能场景)
```go
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 脚本初始化数据库,与 [数据库设计](file:///home/wonder/Code/obsidian/金山办公作业/Week05/数据库设计.md) 文档保持一致。
### 事务与并发
```go
// ❌ 错误:多个 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 共享。
### 错误处理
```go
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 禁用于生产 |
## 关联笔记
- [[DEV/GO/README]]
- [[金山办公作业/Week05/数据库设计]]
- [[金山办公作业/Week05/全栈开发实战:AI 智能单词本]]