vault backup: 2026-04-28 20:56:51
This commit is contained in:
+41
-12
@@ -1,5 +1,5 @@
|
||||
---
|
||||
tags: [GORM, Go, ORM, 关联查询, Preload, Joins, HasOne, HasMany, BelongsTo, ManyToMany]
|
||||
tags: [GORM, Go, ORM, 关联查询, Preload, Joins, HasOne, HasMany, BelongsTo, ManyToMany, Association]
|
||||
create time: 2026-04-28 00:00
|
||||
---
|
||||
|
||||
@@ -7,7 +7,7 @@ create time: 2026-04-28 00:00
|
||||
|
||||
## 概述
|
||||
|
||||
现实世界的数据从不孤立存在——用户有订单、订单包含商品、商品属于分类。如何在一次或多次查询中高效地加载这些关联数据,是 ORM 的核心能力。
|
||||
现实世界的数据从不孤立存在——用户有订单、订单包含商品、商品属于分类。本文介绍 GORM 中如何通过 struct tag 声明模型间关系,以及加载这些关联数据的两种核心策略:**预加载**(N+1 条 SQL)和 **JOIN 单查询**。内容覆盖 HasOne、HasMany、BelongsTo、ManyToMany 四种关联类型的定义与操作。
|
||||
|
||||
GORM 提供了两种主要的关联加载方式:
|
||||
- **N+1 预加载**(`Preload`):额外发 N+1 条 SQL,批量填充关联
|
||||
@@ -152,9 +152,22 @@ db.Preload("Orders", "id, amount, status").
|
||||
>
|
||||
> > **答案**:关联字段会是空切片/零值结构体,不会报错。这是符合直觉的行为——只是「没找到」而非「出错了」。
|
||||
|
||||
---
|
||||
|
||||
## Preload vs PreloadWith
|
||||
|
||||
GORM v1.x 有单独的 `PreloadWith` 方法,但在 v2.x 中已被整合到 `Preload` 的函数式参数里。如果你从旧文档看到 `PreloadWith`,请参考上面的「链式 Preload」写法。
|
||||
> [!warning] 版本注意
|
||||
> GORM v1.x 有单独的 `PreloadWith` 方法用于设置预加载选项,但在 v2.x 中已被整合到 `Preload` 的函数式参数里。如果你从旧文档看到 `PreloadWith`,请参考上面的「带条件的 Preload」写法。
|
||||
|
||||
```go
|
||||
// v1.x(已废弃)
|
||||
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("amount > ?", 100)
|
||||
}).Find(&users)
|
||||
|
||||
// v2.x(推荐 — 用第二个 Preload 调用传递选项)
|
||||
db.Preload("Orders", "amount > ?", 100).Find(&users)
|
||||
```
|
||||
|
||||
## Joins — 单次 JOIN 查询
|
||||
|
||||
@@ -209,6 +222,8 @@ db.Joins("Profile", "profiles.status = ?", "active").First(&user, 1)
|
||||
|
||||
## HasOne 详细示例
|
||||
|
||||
HasOne 表示「一条对应一条」的关系,GORM 通过外键在被关联方查找唯一记录。
|
||||
|
||||
```go
|
||||
// 查询用户的 Profile
|
||||
var user User
|
||||
@@ -223,8 +238,12 @@ db.Create(&User{
|
||||
// INSERT INTO users ...; INSERT INTO profiles ...
|
||||
```
|
||||
|
||||
上面代码展示了两件事:一是用 `Preload` 加载后可以直接访问嵌套字段;二是 GORM 的 `Create` 会自动级联写入关联的记录——先插入主表再插入从表,你不需要手动分别调用两次 `Create`。
|
||||
|
||||
## HasMany 详细示例
|
||||
|
||||
HasMany 表示「一条对应多条」的关系,返回的是一个切片。注意 `Association("Orders").Count()` 统计的是关联记录的条数,而非业务意义上的「总和」。
|
||||
|
||||
```go
|
||||
// 查询用户的所有订单
|
||||
var user User
|
||||
@@ -240,8 +259,12 @@ db.Model(&user).Association("Orders").Count(&total) // 这会统计行数,不
|
||||
db.Model(&user).Select("COALESCE(SUM(amount), 0)").Scan(&total)
|
||||
```
|
||||
|
||||
这里特意展示了一个常见误区:`Association(...).Count()` 做的是 `COUNT(*)` 而非聚合计算。如果需要求和、平均等统计,应该走 `Model(&user).Select("SUM(amount)")` 的标准查询方式。
|
||||
|
||||
## BelongsTo 详细示例
|
||||
|
||||
BelongsTo 表示「属于」关系,外键在当前方(与 HasOne 相反)。加载时使用 `Preload`,更新时直接修改外键字段。
|
||||
|
||||
```go
|
||||
// 查询订单所属的用户
|
||||
var order Order
|
||||
@@ -253,8 +276,12 @@ order.UserID = 2
|
||||
db.Save(&order) // UPDATE orders SET user_id=2 WHERE id=1
|
||||
```
|
||||
|
||||
这里注意:BelongsTo 的外键在**当前模型**的表里(即 `orders.user_id`),而不是在 Users 表里。这决定了 GORM 生成的是 `SELECT * FROM users WHERE id = ?` 来反向填充 User 字段。
|
||||
|
||||
## ManyToMany 详细示例
|
||||
|
||||
ManyToMany 是四种关联中最复杂的一种——GORM 自动创建一个中间表(join table)来维护关系。你可以像操作普通字段一样通过 `Association` API 增删改查。
|
||||
|
||||
```go
|
||||
// 查询订单包含的商品
|
||||
var order Order
|
||||
@@ -288,6 +315,8 @@ db.Model(&order).Association("Products").Clear()
|
||||
count, _ := db.Model(&order).Association("Products").Count()
|
||||
```
|
||||
|
||||
这段代码展示了 Association API 的五个核心操作:**追加**(Append)、**替换**(Replace)、**删除**(Delete)、**清空**(Clear)和**计数**(Count)。其中 Replace 会先清除旧关联再写入新数据,适合「重新赋值」的场景;而 Append 只新增不删除,适合累积添加。
|
||||
|
||||
> [!important] Association API 一览
|
||||
>
|
||||
> | 方法 | 行为 | SQL 效果 |
|
||||
@@ -302,18 +331,18 @@ count, _ := db.Model(&order).Association("Products").Count()
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[需要加载关联数据] --> Depth{关联层级_}
|
||||
Start["需要加载关联数据"] --> Depth{"是否多层级联"}
|
||||
|
||||
Depth --> |单层浅查询| SimpleQ{数据量大吗_}
|
||||
SimpleQ --> |否| JoinsSingle["Joins — 单条 SQL"]
|
||||
SimpleQ --> |是| PreloadSingle["Preload — 多条小 SQL"]
|
||||
Depth --> |否,单层浅查询| SimpleQ{"数据量大吗"}
|
||||
SimpleQ --> |否| JoinsSingle["Joins - 单条SQL"]
|
||||
SimpleQ --> |是| PreloadSingle["Preload - 多条小SQL"]
|
||||
|
||||
Depth --> |多层级联| MultiQ{每层数据量_}
|
||||
MultiQ --> |都很小| DeepJoins["嵌套 Joins"]
|
||||
MultiQ --> |某层较大| DeepPreload["Preload 链式 + 条件过滤"]
|
||||
MultiQ --> |不清楚| PreloadSafe["Preload(安全首选)"]
|
||||
Depth --> |是,多层级联| MultiQ{"每层数据量"}
|
||||
MultiQ --> |都很小| DeepJoins["嵌套Joins"]
|
||||
MultiQ --> |某层较大| DeepPreload["Preload链式加条件过滤"]
|
||||
MultiQ --> |不清楚| PreloadSafe["Preload安全首选"]
|
||||
|
||||
Depth --> |只需要计数/特定字段| SpecificField["Selection 或 Count"]
|
||||
Depth --> |只需要计数或特定字段| SpecificField["Selection或Count"]
|
||||
|
||||
style Start fill:#4FC08D,color:#fff
|
||||
style PreloadSafe fill:#3B82F6,color:#fff
|
||||
|
||||
Reference in New Issue
Block a user