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
多型關聯(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。