Gorm 是一个 Go 语言的 ORM,功能比较强大,招新平台的开发就用到了这个包。

关联模式是各类 ORM 难以回避的话题,也是数据库设计必须考虑的东西,所以开个坑记录一下。

目前这篇文章只有预加载部分。

四种关联模式

这个坑不一定会填,但是很重要,所以在这里保存一些有用的资料,另外看时间补充一些自己的理解。

文档:实体关联 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

貌似关联模式是借鉴的 Ruby 里面的概念:Active Record Associations — Ruby on Rails Guides

belong_tohas_one 搞不清楚:Ruby on Rails : Has_One versus Belongs_To (duanesbrain.blogspot.com)

预加载

预加载包括两种方法:JoinsPreload.

前者类似于 SQL 的 JOIN 命令,在实体关联中,仅适用于一对一的关系,例如 belong_tohas_one。而后者则是 GORM 引入的概念,主要作用是在处理某个 Model 时将关联的 Model 中的信息预先读取出来。

关于 Joins 适用范围较小也比较简单,这里不再讨论,下面主要讲一下 Preload

举个例子:

type User struct {
    *gorm.Model

    Username string `json:"username"`
    Password string `json:"password"`
    Type     string `json:"type"`
}

type Score struct {
    *gorm.Model

    UserId uint `json:"user_id"` // Foreign Key
    User   User `gorm:"foreignKey:UserId"`

    Total  int `json:"total"`
    Web    int `json:"web"`
    Bin    int `json:"bin"`
    Pwn    int `json:"pwn"`
    Crypto int `json:"crypto"`
    Misc   int `json:"misc"`
}

这是一个 Belong_to 的关系,即每个 Score 都属于一个 User,换句话说就是每个 User 都有一个 Score。

Preload 基础

假设我正常查询某个 Score 时:

var score Score
DB.Model(Score{}).Take(&Score)

这样取出的 score 里面不会有 User 的信息,也就是说如果我试图通过 score.User.Username 获取相关联的 User 的 Username,得到的只会是 "",原因是数据库里面 Score 表没有保存 User 表的信息,只有一个用于关联的外键 user_id,所以通过一次查询 Score 不可能把 User 的信息取出来。

如果只是偶尔取出一两个关联的字段还可以手动实现,如果实体关联比较复杂那么我们需要使用 Preload 处理,请看例子:

var ScoreList []Score
// 假设当前 ScoreList 中有 4 条记录,其中 user_id 从 1 到 4。
db.Preload("User").Find(&ScoreList)
// 等价于:
// SELECT * FROM users;
// SELECT * FROM scores WHERE user_id IN (1,2,3,4);

在两个 SELECT 执行完毕后,第二条(预加载)指令运行后的结果还会被写入到 ScoreList 中。此时我们调用 ScoreList[0].User.Username 就可以获取到第 1 条记录对应的 User 的信息。

这是最基础的 Preload 用法,接下来我们可以进行拓展。

Preload 高级用法

前三种都很好理解,建议直接点上面链接看官方文档。

最后一种需要注意,在构造匿名函数的时候一定要返回外键(也就是上例中的 UserId),举个例子:

var ScoreList []Score
DB.Preload("User", func(DB *gorm.DB) *gorm.DB {
    return DB.Select("username, type")
}).Find(&ScoreList)

乍一看,这段代码可以实现返回的 ScoreList 中只有 User 的 usernametype,而阻止了 password 这种敏感信息的返回。

但是运行之后会发现 ScoreList 中的 User 仍为空,和没有预加载的效果是一致的。

原因是,Preload 需要依靠外键进行实体之间的匹配,所以上面第三行应该改为:

return DB.Select("id, username, type"),

最后重复一下:只有把 id (上例中 Score 的外键)也返回才能实现预加载,否则只是简单 SELECT 一下,没有实际效果!