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 。
