模型关联

简介

数据库表通常相互关联。例如,一篇博客文章可能有许多评论,或者一个订单对应一个下单用户。Orm 让这些关联的管理和使用变得简单,并支持多种常用的关联类型:

定义关联

一对一

一对一是最基本的数据库关系。 例如,一个 User 模型可能与一个 Phone 模型相关联。为了定义这个关联关系,我们要在 User 模型中定义一个 Phone

type User struct {
  orm.Model
  Name  string
  Phone   *Phone
}

type Phone struct {
  orm.Model
  UserID   uint
  Name   string
}

Orm 基于父模型(User)的名称来确定关联模型(Phone)的外键名称。在本例中,会自动假定 Phone 模型有一个 UserID 的外键。如果你想重写这个约定,可以在 User 模型中为 Phone 字段添加 foreignKey Tag(其他关联关系类同):

type User struct {
  orm.Model
  Name  string
  Phone   *Phone `gorm:"foreignKey:UserName"`
}

type Phone struct {
  orm.Model
  UserName string
  Name   string
}

另外,Orm 假设外键的值是与父模型的主键(Primary Key)相同的。换句话说,Orm 将会通过 Phone 记录的 UserID 列中查找与用户表的 id 列相匹配的值。如果你希望使用自定义的主键值,可以在 User 模型中为 Phone 字段添加 references Tag(其他关联关系类同):

type User struct {
  orm.Model
  Name  string
  Phone   *Phone `gorm:"foreignKey:UserName;references:name"`
}

type Phone struct {
  orm.Model
  UserName string
  Name   string
}

定义反向关联

我们已经能从 User 模型访问到 Phone 模型了。接下来,让我们再在 Phone 模型上定义一个关联,它能让我们访问到拥有该电话的用户。我们可以在 Phone 模型中定义一个 User 字段:

type User struct {
  orm.Model
  Name  string
}

type Phone struct {
  orm.Model
  UserID   uint
  Name   string
  User   *User
}

一对多

当要定义一个模型是其他 (一个或者多个)模型的父模型这种关系时,可以使用一对多关联。例如,一篇博客可以有很多条评论。和一对一模型关联一样,一对多关联也是在 Orm 模型文件中定义一个字段:

type Post struct {
  orm.Model
  Name   string
  Comments []*Comment
}

type Comment struct {
  orm.Model
  PostID   uint
  Name   string
}

注意,Orm 将会自动为 Comment 模型选择一个合适的外键。通常,这个外键是通过使用父模型的「驼峰命名」方式,然后再加上 ID 的方式来命名的。因此,在上面这个例子中,Orm 将会默认 Comment 模型的外键是 PostID 字段。

一对多 (反向) / 属于

目前我们可以访问一篇文章的所有评论,下面我们可以定义一个关联关系,从而让我们可以通过一条评论来获取到它所属的文章。这个关联关系是 One To Many 的反向,可以在子模型中通过定义父模型字段:

type Post struct {
  orm.Model
  Name   string
  Comments []*Comment
}

type Comment struct {
  orm.Model
  PostID   uint
  Name   string
  Post   *Post
}

多对多关联

多对多关联比 hasOnehasMany 关联稍微复杂些。举个例子,一个用户可以拥有多个角色,同时这些角色也可以分配给其他用户。例如,一个用户可是「作者」和「编辑」;当然,这些角色也可以分配给其他用户。所以,一个用户可以拥有多个角色,一个角色可以分配给多个用户。

表结构

要定义这种关联,需要三个数据库表: users, rolesrole_userrole_user 表的命名可以自定义。该表包含了 user_idrole_id 字段,用作链接 usersroles 的中间表。

特别提醒,由于角色可以属于多个用户,因此我们不能简单地在 roles 表上放置 user_id 列。如果这样,这意味着角色只能属于一个用户。为了支持将角色分配给多个用户,需要使用 role_user 表。我们可以这样定义表结构:

users
  id - integer
  name - string

roles
  id - integer
  name - string

role_user
  user_id - integer
  role_id - integer

模型结构

我们可以在 User 模型上定义一个 Roles 字段:

type User struct {
  orm.Model
  Name  string
  Roles   []*Role `gorm:"many2many:role_user"`
}

type Role struct {
  orm.Model
  Name   string
}

定义反向关联

要定义多对多的反向关联,只需要在 Role 模型中定义 Users 字段并附加 Tag:

type User struct {
  orm.Model
  Name  string
  Roles   []*Role `gorm:"many2many:role_user"`
}

type Role struct {
  orm.Model
  Name   string
  Users  []*User `gorm:"many2many:role_user"`
}

自定义中间表

通常,中间表外键是通过使用父模型的「蛇形」方式,然后再加上 _id,您可以使用 joinForeignKeyjoinReferences 对它进行重写:

type User struct {
  orm.Model
  Name  string
  Roles   []*Role `gorm:"many2many:role_user;joinForeignKey:UserName;joinReferences:RoleName"`
}

type Role struct {
  orm.Model
  Name   string
}

对应表结构:

users
  id - integer
  name - string

roles
  id - integer
  name - string

role_user
  user_name - integer
  role_name - integer

多态

多态关联允许目标模型借助单个关联从属于多个模型。例如,你正在构建一个允许用户共享博客文章和视频的应用程序,其中 Comment 模型可能同时从属于 PostVideo 模型。目前仅有 One To OneOne To Many 支持多态。

表结构

多态关联与简单的关联类似,不过,目标模型能够在一个关联上从属于多个模型。例如,博客 PostVideo 可能共享一个关联到 Image 模型的关系,同时共享多个关联到 Comment 模型。让我们先看看表结构:

posts
  id - integer
  name - string

videos
  id - integer
  name - string

images
  id - integer
  url - string
  imageable_id - integer
  imageable_type - string

comments
  id - integer
  body - text
  commentable_id - integer
  commentable_type - string

要特别留意 images 表的 imageable_idimageable_type 列。imageable_id 列包含文章或用户的 ID 值,而 imageable_type 列包含的则是父模型的类名。Orm 在访问 imageable 时使用 imageable_type 列来判断父模型的「类型」。comments 表类同。

模型结构

接下来,再看看建立关联的模型定义:

type Post struct {
  orm.Model
  Name     string
  Image    *Image `gorm:"polymorphic:Imageable"`
  Comments []*Comment `gorm:"polymorphic:Commentable"`
}

type Video struct {
  orm.Model
  Name     string
  Image    *Image `gorm:"polymorphic:Imageable"`
  Comments []*Comment `gorm:"polymorphic:Commentable"`
}

type Image struct {
  orm.Model
  Name          string
  ImageableID   uint
  ImageableType string
}

type Comment struct {
  orm.Model
  Name            string
  CommentableID   uint
  CommentableType string
}

您可以使用标签 polymorphicValue 来更改多态类型的值,例如:

type Post struct {
  orm.Model
  Name  string
  Image   *Image `gorm:"polymorphic:Imageable;polymorphicValue:master"`
}

关联操作

假设有一个博客系统,它的 User 模型有许多关联的 Post 模型:

type User struct {
  orm.Model
  Name   string
  Posts  []*Post
}

type Post struct {
  orm.Model
  UserID   uint
  Name     string
}

创建/更新关联

可以使用 Select, Omit 方法,对关联的创建和更新进行更细颗粒度控制。这两个方法不可同时使用,且对关联的控制功能只适用于 Create, Update, Save

user := models.User{Name: "user", Posts: []*models.Post{{Name: "post"}}}

// 创建 User 的同时创建所有子关联
facades.Orm().Query().Select(orm.Associations).Create(&user)

// 创建 User 的同时只创建 Post 子关联。注意:如果不使用 `orm.Associations`,而是单独自定义特定子关联,则此时也应将所有父模型中的字段列出。
facades.Orm().Query().Select("Name", "Posts").Create(&user)

// 创建 User 时,忽略 Post 关联,但创建其他所有子关联
facades.Orm().Query().Omit("Posts").Create(&user)

// 创建 User 时,忽略 Name 字段,但创建所有子关联
facades.Orm().Query().Omit("Name").Create(&user)

// 创建 User 时,忽略 Name 字段与所有子关联
facades.Orm().Query().Omit("Name", orm.Associations).Create(&user)

查找关联

// 查找所有匹配的关联记录
var posts []models.Post
facades.Orm().Query().Model(&user).Association("Posts").Find(&posts)

// 查找带条件的关联
facades.Orm().Query().Model(&user).Where("name = ?", "goravel").Order("id desc").Association("Posts").Find(&posts)

添加关联

manyToMany, hasMany 添加新的关联;为 hasOne, belongsTo 替换当前的关联:

facades.Orm().Query().Model(&user).Association("Posts").Append([]*models.Post{Post1, Post2})

facades.Orm().Query().Model(&user).Association("Posts").Append(&models.Post{Name: "goravel"})

替换关联

用一个新的关联替换当前的关联:

facades.Orm().Query().Model(&user).Association("Posts").Replace([]*models.Post{Post1, Post2})

facades.Orm().Query().Model(&user).Association("Posts").Replace(models.Post{Name: "goravel"}, Post2)

删除关联

如果关联存在,则删除父模型与子模型之间的关系,注意,只会删除引用,不会从数据库中删除这些对象,外键需要允许为 NULL:

facades.Orm().Query().Model(&user).Association("Posts").Delete([]*models.Post{Post1, Post2})

facades.Orm().Query().Model(&user).Association("Posts").Delete(Post1, Post2)

清空关联

删除父模型与子模型之间的所有引用,但不会删除这些关联:

facades.Orm().Query().Model(&user).Association("Posts").Clear()

关联计数

返回当前关联的数量:

facades.Orm().Query().Model(&user).Association("Posts").Count()

// 条件计数
facades.Orm().Query().Model(&user).Where("name = ?", "goravel").Association("Posts").Count()

批量处理数据

// 查询所有用户的所有文章
facades.Orm().Query().Model(&users).Association("Posts").Find(&posts)

// 从所有 Post 中删除 user A
facades.Orm().Query().Model(&users).Association("Posts").Delete(&userA)

// 获取去重的用户所属 Post 数量
facades.Orm().Query().Model(&users).Association("Posts").Count()

// 对于批量数据的 `Append`、`Replace`,参数的长度必须与数据的长度相同,否则会返回 error
var users = []models.User{user1, user2, user3}

// 有三个 user,Append userA 到 user1 的 team,Append userB 到 user2 的 team,Append userA、userB 和 userC 到 user3 的 team
facades.Orm().Query().Model(&users).Association("Team").Append(&userA, &userB, &[]models.User{userA, userB, userC})

// 重置 user1 team 为 userA,重置 user2 的 team 为 userB,重置 user3 的 team 为 userA、 userB 和 userC
facades.Orm().Query().Model(&users).Association("Team").Replace(&userA, &userB, &[]models.User{userA, userB, userC})

预加载

预加载为多个模型的查询提供方便,同时减轻了 N + 1 查询问题。 为了说明 N + 1 查询问题,请参考属于 Author 模型的 Book 模型:

type Author struct {
  orm.Model
  Name  string
}

type Book struct {
  orm.Model
  AuthorID   uint
  Name       string
  Author     *Author
}

我们检索所有书籍及其作者:

var books models.Book
facades.Orm().Query().Find(&books)

for _, book := range books {
  var author models.Author
  facades.Orm().Query().Find(&author, book.AuthorID)
}

该循环将执行一个查询以检索数据库表中的所有书籍,然后对每本书执行另一个查询以检索该书的作者。 因此,如果我们有 25 本书,上面的代码将运行 26 个查询:一个查询原本的书籍信息,另外 25 个查询来检索每本书的作者。

值得庆幸的是,我们可以使用预加载将这个操作减少到两个查询。 在构建查询时,可以使用 with 方法指定应该预加载哪些关系:

var books models.Book
facades.Orm().Query().With("Author").Find(&books)

for _, book := range books {
  fmt.Println(book.Author)
}

对于此操作,将只执行两个查询 - 一个查询检索所有书籍,一个查询检索所有书籍的所有作者:

select * from `books`;

select * from `authors` where `id` in (1, 2, 3, 4, 5, ...);

预加载多个关联

有时,你可能需要在单一操作中预加载几个不同的关联。要达成此目的,只需要多次调用 With 方法:

var book models.Book
facades.Orm().Query().With("Author").With("Publisher").Find(&book)

嵌套预加载

可以使用 「点」 语法预加载嵌套关联。比如在一个 Orm 语句中预加载所有书籍作者及其联系方式:

var book models.Book
facades.Orm().Query().With("Author").With("Author.Contacts").Find(&book)

为预加载添加约束

有时,你可能希望预加载一个关联,同时为预加载查询添加额外查询条件。您可以通过下面方法来实现这一点:

import "github.com/goravel/framework/contracts/database/orm"

var book models.Book
facades.Orm().Query().With("Author", "name = ?", "author").Find(&book)

facades.Orm().Query().With("Author", func(query orm.Query) orm.Query {
  return query.Where("name = ?", "author")
}).Find(&book)

在这个例子中,Orm 只会预加载作者的 name 列等于 author 的书籍。

延迟预加载

有时你可能需要在已检索到父模型后立即加载关系。例如,你需要动态决定是否加载相关模型,这可能很有用:

var books models.Book
facades.Orm().Query().Find(&books)

for _, book := range books {
  if someCondition {
    err := facades.Orm().Query().Load(&book, "Author")
  }
}

如果要在延迟预加载的查询语句中进行条件约束,可以使用如下写法:

import "github.com/goravel/framework/contracts/database/orm"

var book models.Book
facades.Orm().Query().Load(&book, "Author", "name = ?", "author").Find(&book)

facades.Orm().Query().Load(&book, "Author", func(query orm.Query) orm.Query {
  return query.Where("name = ?", "author")
}).Find(&book)

如果希望仅加载未被加载的关联关系时,你可以使用 LoadMissing 方法:

facades.Orm().Query().LoadMissing(&book, "Author")