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

例如:

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

has_many 的设定

class_name

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

  1. class Event < ApplicationRecord
  2. has_many :attendees
  3. has_many :paid_attendees, :class_name => "Attendee"
  4. #...
  5. end

foreign_key

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

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

scope

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

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

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

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

dependent

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

  1. class Event < ApplicationRecord
  2. has_many :attendees, :dependent => :destroy
  3. end

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

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

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

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

through

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

  1. class Event < ApplicationRecord
  2. has_many :event_groupships
  3. has_many :groups, :through => :event_groupships
  4. end

source

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

  1. class Event < ApplicationRecord
  2. has_many :event_groupships
  3. has_many :classifications, :through => :event_groupships, :source => :group
  4. end

has_one 的集合物件

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

  • build_{association_name}
  • create_{association_name}

例如:

  1. e = Event.first
  2. e.build_location

has_one 的设定

classnamedependent、_scope条件等设定,都和has_many一样。

belongs_to 的设定

optional

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

  1. class Event < ApplicationRecord
  2. belongs_to :category, :optional => true
  3. end

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

class_name

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

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

foreign_key

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

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

touch

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

  1. class Attendee < ApplicationRecord
  2. belongs_to :event, :touch => true
  3. end

counter_cache

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

  1. class Attendee < ApplicationRecord
  2. belongs_to :event, :counter_cache => true
  3. end

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

joins 和 includes 查询

针对Model中的belongstohas_many关连,可以使用joins,也就是_INNER JOIN

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

可以一次关连多个:

  1. Event.joins(:category, :location)

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

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

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

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

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

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

includes方法也可以加上条件:

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

SQL Explain

多型关联(Polymorphic Associations)

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

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

  1. rails g model comment content:text commentable_id:integer commentable_type

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

  1. class CreateComments < ActiveRecord::Migration[5.1]
  2. def change
  3. create_table :comments do |t|
  4. t.text :content
  5. t.integer :commentable_id
  6. t.string :commentable_type
  7. t.timestamps
  8. end
  9. end
  10. end

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

  1. class CreateComments < ActiveRecord::Migration[5.1]
  2. def change
  3. create_table :comments do |t|
  4. t.text :content
  5. t.belongs_to :commentable, :polymorphic => true
  6. t.timestamps
  7. end
  8. end
  9. end

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

  1. class Comment < ApplicationRecord
  2. belongs_to :commentable, :polymorphic => true
  3. end
  4. class Article < ApplicationRecord
  5. has_many :comments, :as => :commentable
  6. end
  7. class Photo < ApplicationRecord
  8. has_many :comments, :as => :commentable
  9. end

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

  1. article = Article.first
  2. # 透过关连新增留言
  3. comment = article.comments.create(:content => "First Comment")
  4. # 你可以发现 Rails 很聪明的帮我们指定了被留言物件的种类和id
  5. comment.commentable_type => "Article"
  6. comment.commentable_id => 1
  7. # 也可以透过 commentable 反向回查关连的物件
  8. comment.commentable => #<Article id: 1, ....>

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

更多线上资源