Ruby on Rails 實戰聖經

使用 Rails 5.0+ 及 Ruby 2.3+

電子書製作中,歡迎留下 E-mail,有消息將會通知您。若您有任何意見、鼓勵或勘誤,也歡迎來信給我。願意贊助支持的話,这是我的支付宝微信 和乙太幣 ETH 地址
0x232b7245EBE02900c21682be1e6Ad4e839751F6a

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也一併刪除,並且執行Attendeedestroy回呼
  • :delete 把依賴的attendees也一併刪除,但不執行Attendeedestroy回呼
  • :nullify 這是預設值,不會幫忙刪除attendees,但會把attendees的外部鍵event_id都設成NULL
  • :restrict_with_exception 如果有任何依賴的attendees資料,則連event都不允許刪除。執行刪除時會丟出錯誤例外ActiveRecord::DeleteRestrictionError
  • :restrict_with_error 不允許刪除。執行刪除時會回傳false,在@event.errors中會留有錯誤訊息。

要不要執行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_namedependentscope條件等設定,都和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_tohas_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,我們會偏好使用includesincludes會將關連物件的資料也一併讀取出來,避免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上,假設我們已經有了ArticlePhoto這兩個Model,然後我們希望這兩個Model都可以被留言。不用多型關連的話,你得分別建立ArticleCommentPhotoCommentmodel。用多型關連的話,無論有多少種需要被留言的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用來儲存被留言的物件的idcommentable_type則用來儲存被留言物件的種類,以這個例子來說被留言的對象就是ArticlePhoto這兩種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

更多線上資源

》回到頁首