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
可以变更关联的类别名称,例如以下新增了paidattendees
关联,和另一个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
:restrictwith_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 的设定
classname
、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中的belongsto
和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
多型关联(Polymorphic Associations)
多型关连(Polymorphic Associations)可以让一个 Model 不一定关连到某一个特定的 Model,秘诀在于除了整数的id
外部键之外,再加一个字串的_type
字段说明是哪一种_Model。
例如一个Comment model
,我们可以透过多型关连让它belongsto
到各种不同的 _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。