Active Record: 资料表关联

ActiveRecord - 资料表关联

Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. — Brian W. Kernighan

在「ActiveRecord - 基本操作与关联设计」一章我们已经有了关联设计的基本概念,这一章我们将进一步深入了解细节的设定,以及多型关联设计。

has_many 的集合物件

在关联的集合上,我们有以下方法可以使用:

  • «(*records) and create
  • any? and empty?
  • build and new
  • count
  • delete_all
  • destroy_all
  • find(id)
  • ids
  • include?(record)
  • first, last
  • reload

例如:

> e = Event.first
> e.attendees.destroy_all

has_many 的设定

class_name

可以变更关联的类别名称,例如以下新增了paid _attendees_ 关联,和另一个has_many :attendees都关联到同一个_attendees table:

class Event < ApplicationRecord
    has_many :attendees
    has_many :paid_attendees, :class_name => "Attendee"
    #...
end

foreign_key

可以变更 Foreign Key 的字段名称,例如改成paid_user_id:

class Event < ApplicationRecord
    belongs_to :paid_user, :class_name => "User", :foreign_key => "paid_user_id"
    #...
end

scope

在第二个参数传入匿名函式,可以设定关联的范围条件,例如:

class Event < ApplicationRecord
    has_many :attendees
    has_many :paid_attendees, -> { where(:status => "paid") }, :class_name => 'Attendee'
    #...
end

这个语法跟我们之前学过的 Arel 串接写法是一样的,所以可以继续串接加上排序等其他条件:

class Event < ApplicationRecord
    has_many :attendees
    has_many :paid_attendees, -> { where(:status => "paid").order("id DESC") }, :class_name => 'Attendee'
    #...
end

dependent

可以设定当物件删除时,怎么处理依赖它的资料,例如:

class Event < ApplicationRecord
  has_many :attendees, :dependent => :destroy
end

其中:dependent可以设定有几种不同的处理方式,例如:

  • :destroy 把依赖的 attendees 也一并删除,并且执行 Attendee 的 destroy 回呼
  • :delete 把依赖的 attendees 也一并删除,但不执行 Attendee 的 destroy 回呼
  • :nullify 不会帮忙删除 attendees ,但会把 attendees 的外部键event_id都设成NULL
  • :restrict _with_exception_ 如果有任何依赖的_attendees 资料,则连 event 都不允许删除。执行删除时会丢出错误例外ActiveRecord::DeleteRestrictionError。
  • :restrict_with_error 不允许删除。执行删除时会回传false,在@event.errors中会留有错误讯息。

如果没有设定:dependent的话,就不会特别去处理。

要不要执行 attendee 的删除回呼差在执行效率,如果需要回呼的话,必须一笔笔把 attendee 读取出来变成 attendee 物件,然后呼叫它的 destroy 。如果用:delete的话,只需要一个 SQL 语句就可以删除全部 attendee 了。

through

透过关联来建立另一个关联集合,用于建立多对多的关系。

class Event < ApplicationRecord
    has_many :event_groupships
   has_many :groups, :through => :event_groupships
end

source

搭配through设定使用,当关联的名称不一致的时候,需要加上source指名是哪一种物件。

class Event < ApplicationRecord
    has_many :event_groupships
   has_many :classifications, :through => :event_groupships, :source => :group
end

has_one 的集合物件

多了两个方法可以新增关联物件:

  • build_{association_name}
  • create_{association_name}

例如:

e = Event.first
e.build_location

has_one 的设定

class _name_ 、dependent、_scope_条件等设定,都和 _has_many 一样。

belongs_to 的设定

optional

在 Rails 5.1 之后的版本,belongs_to 关联的 model 默认改成必填了,也就是一定要有。透过 optional => true 可以允许 event 没有 category 的情况。

class Event < ApplicationRecord
  belongs_to :category, :optional => true
end

如果你是从旧版 Rails 升级上来,可以在 config/application.rb 中加入 Rails.application.config.active_record.belongs_to_required_by_default = false 改回旧版的默认行为。

class_name

可以变更关联的类别名称,例如:

class Event < ApplicationRecord
    belongs_to :manager, :class_name => "User" # 默认的外部键叫做 manager_id
end

foreign_key

可以变更 Foreign Key 的字段名称,例如改成user_id:

class Event < ApplicationRecord
    belongs_to :manager, :class_name => "User", :foreign_key => "user_id"
end

touch

这会在修改时,也顺道修改关联资料的updated_at时间:

class Attendee < ApplicationRecord
    belongs_to :event, :touch => true
end

counter_cache

针对关联作计数的快取,假设 Event 身上有 attendees_count 这个字段,那么:

class Attendee < ApplicationRecord
    belongs_to :event, :counter_cache => true
end

这样 ActiveRecord 就会自动更新 attendees_count 的数字。

joins 和 includes 查询

针对 Model 中的belongs _to_ 和has_many关连,可以使用joins,也就是_INNER JOIN

Event.joins(:category)
## SELECT "events".* FROM "events" INNER JOIN "categories" ON "categories"."id" = "events"."category_id"

可以一次关连多个:

 Event.joins(:category, :location)

透过 joins 抓出来的 event 物件是没有包括其关连物件的,因为 Rails 默认只有select event.__而没有select categories.,因此 joins 主要的用途是来搭配 where 的条件查询,帮忙过滤 events 资料:

Event.joins(:category).where("categories.name is NOT NULL")
## SELECT "events".* FROM "events" INNER JOIN "categories" ON "categories"."id" = "events"."category_id" WHERE (categories.name is NOT NULL)

如果需要其关连物件的资料,例如上述的 categories ,我们会偏好使用includes。 includes 会将关连物件的资料也一并读取出来,避免 N+1 问题(见效能一章),例如:

Event.includes(:category)
## SELECT * FROM events
## SELECT * FROM categories WHERE categories.id IN (1,2,3...)

同理,也可以一次加载多个关连:

Event.includes(:category, :attendees)
## SELECT "events".* FROM "events"
## SELECT "categories".* FROM "categories" WHERE "categories"."id" IN (1,2,3...)
## SELECT "attendees".* FROM "attendees" WHERE "attendees"."event_id" IN (4, 5, 6, 7, 8...)

includes方法也可以加上条件:

Event.includes(:category).where( :category => { :position => 1 } )

SQL Explain

  • <http://www.rubyletter.com/blog/2017/03/13/rubyist-guide-to-postgres-explain.html>

多型关联(Polymorphic Associations)

多型关连 (Polymorphic Associations) 可以让一个 Model 不一定关连到某一个特定的 Model,秘诀在于除了整数的_id_ 外部键之外,再加一个字串的_type字段说明是哪一种_Model。

例如一个Comment model,我们可以透过多型关连让它belongs _to_ 到各种不同的 _Model 上,假设我们已经有了 Article 与 Photo 这两个 Model ,然后我们希望这两个 Model 都可以被留言。不用多型关连的话,你得分别建立 ArticleComment 和 PhotoComment 的 model 。用多型关连的话,无论有多少种需要被留言的 Model ,只需要一个 Comment model 即可:

rails g model comment content:text commentable_id:integer commentable_type

这样会产生下面的 Migration 档案:

class CreateComments < ActiveRecord::Migration[5.1]
  def change
    create_table :comments do |t|
      t.text :content
      t.integer :commentable_id
      t.string :commentable_type

      t.timestamps
    end
  end
end

这个 Migration 档案中,我们用 content 这个字段来储存留言的内容, commentable_id 用来储存被留言的物件的 id 而 commentable_type 则用来储存被留言物件的种类,以这个例子来说被留言的对象就是 Article 与 Photo 这两种 Model ,这个 Migration 档案也可以改写成下面这样:

class CreateComments < ActiveRecord::Migration[5.1]
  def change
    create_table :comments do |t|
      t.text :content
      t.belongs_to :commentable, :polymorphic => true

      t.timestamps
    end
  end
end

回到我们的 Model ,我们必须指定他们的关联关系:

class Comment < ApplicationRecord
  belongs_to :commentable, :polymorphic => true
end

class Article < ApplicationRecord
  has_many :comments, :as => :commentable
end

class Photo < ApplicationRecord
  has_many :comments, :as => :commentable
end

这样会告诉 Rails 如何去设定你的多型关系,现在让我们进 console 实验看看:

article = Article.first

## 透过关连新增留言
comment = article.comments.create(:content => "First Comment")

## 你可以发现 Rails 很聪明的帮我们指定了被留言物件的种类和id
comment.commentable_type => "Article"
comment.commentable_id => 1

## 也可以透过 commentable 反向回查关连的物件
comment.commentable => #<Article id: 1, ....>

DBA 背景的同学可能会注意到 PolymorphicAassociations 无法做到保证 Referential integrity 特性。原因很简单,既然不知道_id会指到哪个table,自然也就没办法在数据库层级加上 Foreign key constraint 。

更多线上资源