跳至內容 跳至搜尋

Active Record 關聯

關聯是一種大型類別方法組合,透過外來金鑰連結物件在一起。它們表達類似「專案擁有一個專案管理員」或「專案屬於一個投資組合」的關聯性。每個大型函式會新增一些方法至類別,這些方法會針對集合或關聯符號以及選項雜湊進行專門化。它的運作方式與 Ruby 本身的 attr* 方法非常相似。

class Project < ActiveRecord::Base
  belongs_to              :portfolio
  has_one                 :project_manager
  has_many                :milestones
  has_and_belongs_to_many :categories
end

專案類別現在有下列方法(以及其他方法)來簡化其關係的遍歷與處理:

project = Project.first
project.portfolio
project.portfolio = Portfolio.first
project.reload_portfolio

project.project_manager
project.project_manager = ProjectManager.first
project.reload_project_manager

project.milestones.empty?
project.milestones.size
project.milestones
project.milestones << Milestone.first
project.milestones.delete(Milestone.first)
project.milestones.destroy(Milestone.first)
project.milestones.find(Milestone.first.id)
project.milestones.build
project.milestones.create

project.categories.empty?
project.categories.size
project.categories
project.categories << Category.first
project.categories.delete(category1)
project.categories.destroy(category1)

警告

請勿建立與 ActiveRecord::Base 執行個體方法 同名的關聯。由於關聯會針對模型新增含有此名稱的方法,因此使用與 ActiveRecord::Base 所提供的名稱同名的關聯會覆寫由 ActiveRecord::Base 繼承而來的該方法,並導致錯誤發生。例如,attributesconnection 會是作為關聯名稱的不當選擇,因為這些名稱已存在於 ActiveRecord::Base 執行個體方法清單中。

自動產生方法

另見下方「執行個體公開方法」(來自 belongs_to)以了解詳細資料。

單數關聯(一對一)

                                  |            |  belongs_to  |
generated methods                 | belongs_to | :polymorphic | has_one
----------------------------------+------------+--------------+---------
other                             |     X      |      X       |    X
other=(other)                     |     X      |      X       |    X
build_other(attributes={})        |     X      |              |    X
create_other(attributes={})       |     X      |              |    X
create_other!(attributes={})      |     X      |              |    X
reload_other                      |     X      |      X       |    X
other_changed?                    |     X      |      X       |
other_previously_changed?         |     X      |      X       |

集合關聯(一對多/多對多)

                                  |       |          | has_many
generated methods                 | habtm | has_many | :through
----------------------------------+-------+----------+----------
others                            |   X   |    X     |    X
others=(other,other,...)          |   X   |    X     |    X
other_ids                         |   X   |    X     |    X
other_ids=(id,id,...)             |   X   |    X     |    X
others<<                          |   X   |    X     |    X
others.push                       |   X   |    X     |    X
others.concat                     |   X   |    X     |    X
others.build(attributes={})       |   X   |    X     |    X
others.create(attributes={})      |   X   |    X     |    X
others.create!(attributes={})     |   X   |    X     |    X
others.size                       |   X   |    X     |    X
others.length                     |   X   |    X     |    X
others.count                      |   X   |    X     |    X
others.sum(*args)                 |   X   |    X     |    X
others.empty?                     |   X   |    X     |    X
others.clear                      |   X   |    X     |    X
others.delete(other,other,...)    |   X   |    X     |    X
others.delete_all                 |   X   |    X     |    X
others.destroy(other,other,...)   |   X   |    X     |    X
others.destroy_all                |   X   |    X     |    X
others.find(*args)                |   X   |    X     |    X
others.exists?                    |   X   |    X     |    X
others.distinct                   |   X   |    X     |    X
others.reset                      |   X   |    X     |    X
others.reload                     |   X   |    X     |    X

覆寫產生的方法

關聯方法會產生於包含在模型類別中的模組中,讓覆寫變得容易。因此,可使用 super 來呼叫原始產生的方法。

class Car < ActiveRecord::Base
  belongs_to :owner
  belongs_to :old_owner

  def owner=(new_owner)
    self.old_owner = self.owner
    super
  end
end

關聯方法模組會在產生屬性方法模組後立即包含進來,意即關聯會覆寫帶有相同名稱的屬性方法。

主從性和關聯性

Active Record 關聯可用於描述模型之間的一對一、一對多和多對多關聯性。每個模型會使用關聯來描述其在關聯性中的角色。

一對一

在基礎部分使用 has_one,在關聯模型中使用 belongs_to

class Employee < ActiveRecord::Base
  has_one :office
end
class Office < ActiveRecord::Base
  belongs_to :employee    # foreign key - employee_id
end

一對多

在基礎部分使用 has_many,在關聯模型中使用 belongs_to

class Manager < ActiveRecord::Base
  has_many :employees
end
class Employee < ActiveRecord::Base
  belongs_to :manager     # foreign key - manager_id
end

多對多

建立多對多關聯性的方法有兩種。

第一種方式是使用帶有 :through 選項和聯結模型的 has_many 關聯,因此會有兩個關聯階段。

class Assignment < ActiveRecord::Base
  belongs_to :programmer  # foreign key - programmer_id
  belongs_to :project     # foreign key - project_id
end
class Programmer < ActiveRecord::Base
  has_many :assignments
  has_many :projects, through: :assignments
end
class Project < ActiveRecord::Base
  has_many :assignments
  has_many :programmers, through: :assignments
end

對於第二種方式,在兩個模型中使用 has_and_belongs_to_many。這需要無對應模型或主鍵的聯結表格。

class Programmer < ActiveRecord::Base
  has_and_belongs_to_many :projects       # foreign keys in the join table
end
class Project < ActiveRecord::Base
  has_and_belongs_to_many :programmers    # foreign keys in the join table
end

選擇建立多對多關聯性的方式並非總是簡單。如果您需要將關聯模型當成其自己的實體處理,請使用 has_many :through。如果您使用舊有模式,或從未直接處理關聯本身,請使用 has_and_belongs_to_many

它是 belongs_to 還是 has_one 關聯?

這兩個都表達 1-1 的關係。差異主要在於外來鍵放置的位置,外來鍵會出現在宣告 belongs_to 關係的類別中。

class User < ActiveRecord::Base
  # I reference an account.
  belongs_to :account
end

class Account < ActiveRecord::Base
  # One user references me.
  has_one :user
end

這些類別的資料表可能如下所示

CREATE TABLE users (
  id bigint NOT NULL auto_increment,
  account_id bigint default NULL,
  name varchar default NULL,
  PRIMARY KEY  (id)
)

CREATE TABLE accounts (
  id bigint NOT NULL auto_increment,
  name varchar default NULL,
  PRIMARY KEY  (id)
)

未儲存的物件和相關性

可以在物件和相關性儲存至資料庫前對其進行操作,但有一些特別的行為您應該知道,主要與關聯物件的儲存有關。

您可以設定 has_onebelongs_tohas_manyhas_and_belongs_to_many 相關性的 :autosave 選項。將其設為 true永遠儲存成員,而將其設為 false永遠不會儲存成員。有關 :autosave 選項的更多詳細資訊,請參閱 AutosaveAssociation

一對一相關性

  • 將物件指派給 has_one 相關性會自動儲存該物件和被取代的物件(如果存在),以更新它們的外來鍵(除非父物件未儲存(new_record? == true))。

  • 如果上述任何一個儲存失敗(因為其中一個物件無效),則會引發 ActiveRecord::RecordNotSaved 例外,並且指派會被取消。

  • 如果您希望將物件指派給 has_one 相關性而不儲存它,請使用 #build_association 方法(文件如下)。被取代的物件仍會儲存以更新其外來鍵。

  • 將物件指派給 belongs_to 相關性不會儲存該物件,因為外來鍵欄位屬於父物件。它也不會儲存父物件。

集合

  • 將物件新增至集合(has_manyhas_and_belongs_to_many)會自動儲存該物件,除非父物件(集合的所有者)尚未儲存在資料庫中。

  • 如果將任何物件新增至集合(透過 push 或類似方式)時儲存失敗,則 push 會傳回 false

  • 如果在取代集合時(透過 association=)儲存失敗,則會引發 ActiveRecord::RecordNotSaved 例外,並且指派會被取消。

  • 您可以使用 collection.build 方法(文件如下)將物件新增至集合而不自動儲存它。

  • 集合的所有未儲存成員(new_record? == true)會在儲存父項目時自動儲存。

自訂查詢

相關性由 Relation 物件建立,您可以使用 Relation 句法來自訂它們。例如,要新增條件

class Blog < ActiveRecord::Base
  has_many :published_posts, -> { where(published: true) }, class_name: 'Post'
end

-> { ... } 區塊中,您可以使用所有典型的 Relation 方法。

存取所有者物件

建立查詢時,有時需要存取所有者物件。所有者會作為參數傳遞至區塊。例如,以下相關性會找出發生於使用者生日的所有活動

class User < ActiveRecord::Base
  has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event'
end

注意:無法聯結或熱切載入此類相關性,因為這些操作會在執行個體建立前發生。此類相關性可以預先載入,但這麼做會執行 N+1 查詢,因為每筆記錄都會有不同的範圍(類似於預先載入多型態範圍)。

相關性回呼

與連入 Active Record 物件生命週期的正常回呼類似,您也可以定義在將物件新增至相關性集合或從中移除物件時觸發的回呼。

class Firm < ActiveRecord::Base
  has_many :clients,
           dependent: :destroy,
           after_add: :congratulate_client,
           after_remove: :log_after_remove

  def congratulate_client(client)
    # ...
  end

  def log_after_remove(client)
    # ...
  end
end

Callbacks 可用三種方式定義

  1. 引用關聯集合定義在類別上的方法的符號。例如,after_add: :congratulate_client 呼叫 Firm#congratulate_client(client)

  2. 可呼叫且含有特徵,可接受關聯集合中的記錄和正在新增或移除的記錄。例如,after_add: ->(firm, client) { ... }

  3. 響應呼叫回名稱的物件。例如,傳遞 after_add: CallbackObject.new 將呼叫 CallbackObject#after_add(firm, client)

可以透過將呼叫回傳遞為陣列堆疊呼叫回。範例

class CallbackObject
  def after_add(firm, client)
    firm.log << "after_adding #{client.id}"
  end
end

class Firm < ActiveRecord::Base
  has_many :clients,
           dependent: :destroy,
           after_add: [
             :congratulate_client,
             -> (firm, client) { firm.log << "after_adding #{client.id}" },
             CallbackObject.new
           ],
           after_remove: :log_after_remove
end

可能的呼叫回為:before_addafter_addbefore_removeafter_remove

如果任何 before_add 呼叫回擲回例外,則不會將物件新增至集合。

類似地,如果任何 before_remove 呼叫回擲回例外,則不會從集合中移除物件。

注意:若要引發移除呼叫回,您必須使用 destroy / destroy_all 方法。例如

  • firm.clients.destroy(client)

  • firm.clients.destroy(*clients)

  • firm.clients.destroy_all

delete / delete_all 方法如下不會引發移除呼叫回

  • firm.clients.delete(client)

  • firm.clients.delete(*clients)

  • firm.clients.delete_all

關聯延伸模組

控制對關聯存取的代理程式物件可以透過匿名的模組延伸。這對於新增僅用在這個關聯中的新尋找器、建立器和其他製造工廠類型的方法特別有幫助。

class Account < ActiveRecord::Base
  has_many :people do
    def find_or_create_by_name(name)
      first_name, last_name = name.split(" ", 2)
      find_or_create_by(first_name: first_name, last_name: last_name)
    end
  end
end

person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name  # => "Heinemeier Hansson"

如果您需要在許多關聯之間共用相同的延伸模組,您可以使用命名延伸模組。

module FindOrCreateByNameExtension
  def find_or_create_by_name(name)
    first_name, last_name = name.split(" ", 2)
    find_or_create_by(first_name: first_name, last_name: last_name)
  end
end

class Account < ActiveRecord::Base
  has_many :people, -> { extending FindOrCreateByNameExtension }
end

class Company < ActiveRecord::Base
  has_many :people, -> { extending FindOrCreateByNameExtension }
end

一些延伸模組僅能透過了解關聯內部運作才有作用。延伸模組可以使用下列方法存取相關狀態(其中 items 為關聯的名稱)

  • record.association(:items).owner - 傳回關聯中的物件。

  • record.association(:items).reflection - 傳回描述關聯的反射物件。

  • record.association(:items).target - 傳回 belongs_tohas_one 的關聯物件,或 has_manyhas_and_belongs_to_many 關聯物件的集合。

不過,在實際延伸模組程式碼中,您無法存取上述的 record。在这种情况下,您可以访问 `proxy_association`。例如,`record.association(:items)` 和 `record.items.proxy_association` 將傳回同一個物件,讓您可以在關聯延伸模組中呼叫 `proxy_association.owner` 的函式。

關聯加入模型

:through 選項可以設定 Has Many 關聯,以便使用明確的加入模型來擷取資料。這與 has_and_belongs_to_many 關聯的運作方式類似。好處是可以針對加入模型新增驗證、呼叫回和額外屬性。請考量下列結構

class Author < ActiveRecord::Base
  has_many :authorships
  has_many :books, through: :authorships
end

class Authorship < ActiveRecord::Base
  belongs_to :author
  belongs_to :book
end

@author = Author.first
@author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to
@author.books                              # selects all books by using the Authorship join model

您也可以透過加入模型的 has_many 關聯

class Firm < ActiveRecord::Base
  has_many   :clients
  has_many   :invoices, through: :clients
end

class Client < ActiveRecord::Base
  belongs_to :firm
  has_many   :invoices
end

class Invoice < ActiveRecord::Base
  belongs_to :client
end

@firm = Firm.first
@firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
@firm.invoices                            # selects all invoices by going through the Client join model

類似地,您可以透過加入模型的 has_one 關聯

class Group < ActiveRecord::Base
  has_many   :users
  has_many   :avatars, through: :users
end

class User < ActiveRecord::Base
  belongs_to :group
  has_one    :avatar
end

class Avatar < ActiveRecord::Base
  belongs_to :user
end

@group = Group.first
@group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group
@group.avatars                                # selects all avatars by going through the User join model.

在加入模型中使用 has_onehas_many 關聯時,一個重要的注意事項是這些關聯為**唯讀**。例如,在先前的範例中,以下方法將無法使用

@group.avatars << Avatar.new   # this would work if User belonged_to Avatar rather than the other way around
@group.avatars.delete(@group.avatars.last)  # so would this

設定反向

如果使用加入模型中的 belongs_to,最好是在 belongs_to 中設定 :inverse_of 選項,代表以下範例能正常運作(而其中 tagshas_many :through 關聯)

@post = Post.first
@tag = @post.tags.build name: "ruby"
@tag.save

最後一行應該可以儲存加入記錄(Tagging)。這只會在設定 :inverse_of 時運作

class Tagging < ActiveRecord::Base
  belongs_to :post
  belongs_to :tag, inverse_of: :taggings
end

如果沒有設定 :inverse_of 記錄,關聯會盡量比對正確的反向關聯。自動反向偵測只會作用於 has_manyhas_onebelongs_to 關聯。

關聯中的 :foreign_key:through 選項,以及在某些情況下的自訂範圍都會阻止自動尋找關聯中的反向關聯。進一步的詳細資料請參閱 Active Record 關聯指南

自動猜測反向關聯使用的是根據類別名稱的啟發方式,因此可能無法對所有的關聯運作,特別是非標準名稱的那些關聯。

您可以透過將 :inverse_of 選項設為 false 方法關閉反向關聯的自動偵測,如下所示

class Tagging < ActiveRecord::Base
  belongs_to :tag, inverse_of: false
end

巢狀關聯

您實際上可以使用 :through 選項指定任何關聯,包括本身有 :through 選項的關聯。例如

class Author < ActiveRecord::Base
  has_many :posts
  has_many :comments, through: :posts
  has_many :commenters, through: :comments
end

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :commenter
end

@author = Author.first
@author.commenters # => People who commented on posts written by the author

設定此關聯的等價方式如下

class Author < ActiveRecord::Base
  has_many :posts
  has_many :commenters, through: :posts
end

class Post < ActiveRecord::Base
  has_many :comments
  has_many :commenters, through: :comments
end

class Comment < ActiveRecord::Base
  belongs_to :commenter
end

使用巢狀關聯時,您將無法修改關聯,因為沒有足夠的資訊知道要進行哪一種修改。例如,如果您嘗試在上面的範例中新增 Commenter,便無法告知系統要如何設定中介 PostComment 物件。

多型關聯

模型上的多型關聯並未限制其能關聯的模型類型。反之,它們指定 has_many 關聯必須遵守的介面。

class Asset < ActiveRecord::Base
  belongs_to :attachable, polymorphic: true
end

class Post < ActiveRecord::Base
  has_many :assets, as: :attachable         # The :as option specifies the polymorphic interface to use.
end

@asset.attachable = @post

此方法使用類型欄位搭配外來金鑰來指定關聯記錄。在 Asset 範例中,您需要 attachable_id 整數欄位及 attachable_type 字串欄位。

將多型關聯與單一表格繼承(STI)搭配使用時會稍微 tricky 一些。為了讓關聯如預期運作,請務必在多型關聯的類型欄位中儲存 STI 模型的基礎模型。繼續使用上面的資產範例,假設我們有使用 post 表格 for STI 的客座貼文及會員貼文。在這種情況下,post 表格中必須有一個 type 欄位。

注意:在指派 attachable 時會呼叫 attachable_type= 方法。attachableclass_name 會作為 String 傳遞。

class Asset < ActiveRecord::Base
  belongs_to :attachable, polymorphic: true

  def attachable_type=(class_name)
     super(class_name.constantize.base_class.to_s)
  end
end

class Post < ActiveRecord::Base
  # because we store "Post" in attachable_type now dependent: :destroy will work
  has_many :assets, as: :attachable, dependent: :destroy
end

class GuestPost < Post
end

class MemberPost < Post
end

快取

所有方法都建立於一個簡單的快取原則,除非特別指示不要快取,否則它們會保留上一次查詢的結果。此快取甚至跨方法共用,讓您在使用巨集新增的方法時更輕輕鬆鬆,不用太擔心第一次執行的效能。

project.milestones             # fetches milestones from the database
project.milestones.size        # uses the milestone cache
project.milestones.empty?      # uses the milestone cache
project.milestones.reload.size # fetches milestones from the database
project.milestones             # uses the milestone cache

關聯關係的即時載入

即時載入是尋找特定類別物件和多個命名關聯關係的方法。這是避免可怕的 N+1 問題最簡便的方法之一,此問題在擷取每個需要顯示作者的 100 篇文章時會觸發 101 個資料庫查詢。透過使用即時載入,查詢的數量將從 101 減少到 2。

class Post < ActiveRecord::Base
  belongs_to :author
  has_many   :comments
end

考量在上述類別中使用下列迴圈

Post.all.each do |post|
  puts "Post:            " + post.title
  puts "Written by:      " + post.author.name
  puts "Last comment on: " + post.comments.first.created_on
end

若要反覆執行這一百篇文章,我們會產生 201 個資料庫查詢。讓我們先針對擷取作者進行最佳化

Post.includes(:author).each do |post|

這是指也使用 :author 符號的 belongs_to 關聯關係的名稱。在載入文章後,find 會從每篇文章中收集 author_id,並使用一個查詢載入所有參考到的作者。這樣做會將查詢數量從 201 減少到 102。

我們可透過在 finder 中同時參考兩個關聯關係讓情況更進一步,使用

Post.includes(:author, :comments).each do |post|

這樣會使用單一查詢載入所有留言。這將總查詢數量減少到 3。通常,查詢數量會等於 1 加上命名的關聯關係數量(除非某些關聯關係是多型 belongs_to - 請參閱下方)。

若要包含多層階的關聯關係,請使用 hash

Post.includes(:author, { comments: { author: :gravatar } }).each do |post|

上述程式碼會載入所有留言及其所有相關的作者與大頭貼。您可以混合和搭配任何符號、陣列和 hash 組合,以擷取您想載入的關聯關係。

所有這些功能不應讓您誤以為可以因為減少了查詢數量,而毫不費力地擷取大量資料。資料庫仍需要將所有資料傳送給 Active Record,且仍需要進行處理。因此,這並非對於效能問題的萬靈丹,但它是在上方所述的情況下減少查詢數量的好方法。

由於一次只載入一個表格,因此條件或順序無法參考主要表格以外的表格。如果是這樣的話,Active Record 會改用先前所使用,基於 LEFT OUTER JOIN 的策略。例如

Post.includes([:author, :comments]).where(['comments.approved = ?', true])

這會產生一個具有下列連接線的單一 SQL 查詢:LEFT OUTER JOIN comments ON comments.post_id = posts.idLEFT OUTER JOIN authors ON authors.id = posts.author_id。請注意,使用此類條件可能會造成意料之外的後果。在上方的範例中,完全沒有已核准留言的文章不會被傳回,這是因為條件套用至整個 SQL 陳述式,而不僅是關聯關係。

您必須消除欄位參考的歧義,此回退才會發生,例如 order: "author.name DESC" 會運作,但 order: "name DESC" 則不會。

如果您想要載入所有文章(包括沒有已核准留言的文章),請使用 ON 撰寫自己的 LEFT OUTER JOIN 查詢

Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")

在這情況下,包含有定義條件的關聯關係通常比較自然

class Post < ActiveRecord::Base
  has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment'
end

Post.includes(:approved_comments)

這會載入文章,並即時載入 approved_comments 關聯關係,其中僅包含已核准的留言。

如果您對具有特定 :limit 選項的關聯關係進行即時載入,該選項將會被忽略,傳回所有相關物件

class Picture < ActiveRecord::Base
  has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment'
end

Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.

即時載入支援多型關聯關係。

class Address < ActiveRecord::Base
  belongs_to :addressable, polymorphic: true
end

嘗試即時載入可定址模型的呼叫

Address.includes(:addressable)

這會執行一個查詢,用每個對應類型的一個查詢來載入地址和對應對象。例如,如果所有對應對象都是 Person 或 Company 類別,接著總共將執行 3 個查詢。要載入的對應對象類型清單會在載入的地址之後決定。如果 Activa Record 必須降回使用過往的 eager 載入實作,這將不受支援,並且會產生 ActiveRecord::EagerLoadPolymorphicError。原因在於父模型的類型是一欄位值,因此其對應的表格名稱無法放置在該查詢的 FROM/JOIN 子句中。

表格別名

在表格在加入中被多次引用的情況下,Active Record 會使用表格別名。如果一個表格只被引用一次,將會使用標準表格名稱。第二次時,表格會以 #{reflection_name}_#{parent_table_name} 的方式建立別名。對於表格名稱的更多連續使用,將附加索引。

Post.joins(:comments)
# SELECT ... FROM posts INNER JOIN comments ON ...
Post.joins(:special_comments) # STI
# SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment'
Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name
# SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts

作用為樹狀範例

TreeMixin.joins(:children)
# SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
TreeMixin.joins(children: :parent)
# SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
#                        INNER JOIN parents_mixins ...
TreeMixin.joins(children: {parent: :children})
# SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
#                        INNER JOIN parents_mixins ...
#                        INNER JOIN mixins childrens_mixins_2

Has and Belongs to Many 加入表格使用相同的點子,但是加上 _join 字尾

Post.joins(:categories)
# SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
Post.joins(categories: :posts)
# SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
#                       INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
Post.joins(categories: {posts: :categories})
# SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
#                       INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
#                       INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2

如果您希望使用 ActiveRecord::QueryMethods#joins 方法來指定您自己的自訂加入,這些表格名稱將優先於 eager 關聯

Post.joins(:comments).joins("inner join comments ...")
# SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ...
Post.joins(:comments, :special_comments).joins("inner join comments ...")
# SELECT ... FROM posts INNER JOIN comments comments_posts ON ...
#                       INNER JOIN comments special_comments_posts ...
#                       INNER JOIN comments ...

表格別名依照特定資料庫表格識別碼的最大長度自動縮短。

模組

預設上,關聯會在目前模組範圍內尋找物件。考慮以下範例

module MyApplication
  module Business
    class Firm < ActiveRecord::Base
      has_many :clients
    end

    class Client < ActiveRecord::Base; end
  end
end

當呼叫 Firm#clients 時,它會依序呼叫 MyApplication::Business::Client.find_all_by_firm_id(firm.id)。如果您想與另一個模組範圍類別關聯,可以透過指定完整的類別名稱來完成。

module MyApplication
  module Business
    class Firm < ActiveRecord::Base; end
  end

  module Billing
    class Account < ActiveRecord::Base
      belongs_to :firm, class_name: "MyApplication::Business::Firm"
    end
  end
end

雙向關聯

在您指定關聯時,通常在已關聯模型中會有一個以相反關係指定相同關聯的關聯。例如,使用以下模型

class Dungeon < ActiveRecord::Base
  has_many :traps
  has_one :evil_wizard
end

class Trap < ActiveRecord::Base
  belongs_to :dungeon
end

class EvilWizard < ActiveRecord::Base
  belongs_to :dungeon
end

Dungeon 上的 traps 關聯和 Trap 上的 dungeon 關聯是彼此的相反,而 EvilWizard 上的 dungeon 關聯的相反則是 Dungeon 上的 evil_wizard 關聯 (反之亦然)。預設上,Active Record 可以根據類別名稱猜測關聯。結果如下

d = Dungeon.first
t = d.traps.first
d.object_id == t.dungeon.object_id # => true

上述範例中的 Dungeon 實例 dt.dungeon 參考相同的記憶體內實例,因為關聯與類別的名稱相符。如果我們將 :inverse_of 加入到我們的模型定義中,結果會相同

class Dungeon < ActiveRecord::Base
  has_many :traps, inverse_of: :dungeon
  has_one :evil_wizard, inverse_of: :dungeon
end

class Trap < ActiveRecord::Base
  belongs_to :dungeon, inverse_of: :traps
end

class EvilWizard < ActiveRecord::Base
  belongs_to :dungeon, inverse_of: :evil_wizard
end

有關更多資訊,請參閱 :inverse_of 選項的文件和 Active Record 關聯指南

從關聯中刪除

依賴關聯

has_manyhas_onebelongs_to 關聯支援 :dependent 選項。這讓您可以指定在擁有者被刪除時應刪除已關聯記錄。

例如

class Author
  has_many :posts, dependent: :destroy
end
Author.find(1).destroy # => Will destroy all of the author's posts, too

:dependent 選項可以有不同的值,指定如何刪除。有關更多資訊,請參考不同特定關聯類型中這個選項的文件。當沒有給定選項時,行為是在摧毀記錄時不對已關聯記錄有任何動作。

請注意 :dependent 是使用 Rails 的回呼系統實作,其運作方式是依序處理回呼。因此,在 :dependent 選項前或後宣告的其他回呼會影響它執行動作。

請注意,:dependent 選項會被 has_one :through 關聯忽略。

刪除或銷毀?

has_manyhas_and_belongs_to_many 關聯具有 destroydeletedestroy_alldelete_all 方法。

對於 has_and_belongs_to_manydeletedestroy 是相同的:它們會導致關聯資料表中的記錄遭到移除。

對於 has_manydestroydestroy_all 會永遠呼叫正在移除的記錄的 destroy 方法,以執行 callback。然而,deletedelete_all 會根據 :dependent 選項指定的策略執行刪除,或是在沒有提供 :dependent 選項的情況下,遵循預設策略。預設策略是除了 has_many :through 外什麼都不做(讓外來鍵保持設定為父類型的識別碼),delete_allhas_many :through 的預設策略(刪除關聯記錄,而不執行它們的 callback)。

還有一個 clear 方法,它與 delete_all 相同,只是它傳回關聯而不是已刪除的記錄。

會刪除什麼?

這裡有一個潛藏的陷阱:has_and_belongs_to_manyhas_many :through 關聯在關聯資料表和關聯記錄中都有記錄。因此,當我們呼叫其中一個刪除方法時,到底會刪除什麼?

答案是在關聯上執行的刪除,假設是移除擁有者和關聯物件之間的連結,而不是必要的關聯物件本身。因此針對 has_and_belongs_to_manyhas_many :through,關聯記錄會被刪除,但關聯記錄不會。

如果你想一想的話,這是有道理的:假設你要呼叫 post.tags.delete(Tag.find_by(name: 'food')),你會想要從文章中取消和「food」標籤的連結,而不是從資料庫中移除標籤本身。

不過,在某些案例中,此策略並不明智。例如,假設一個人有很多專案,而且每個專案都有很多任務。如果我們刪除某個人的其中一個任務,我們或許不會想要刪除這個專案。在此情況下,刪除方法根本不起作用:它只能在關聯模型上的關聯是 belongs_to 時使用。在其他情況下,你應該直接對關聯記錄或 :through 關聯執行操作。

對於一般的 has_many,不會區分「關聯記錄」和「連結」,因此只有一個選項可以刪除哪些項目。

對於 has_and_belongs_to_manyhas_many :through,如果你想自行刪除關聯記錄,永遠可以執行類似 person.tasks.each(&:destroy) 的操作。

具備 ActiveRecord::AssociationTypeMismatch 的類型安全

如果你嘗試將物件指定給與推論或指定的 :class_name 不相符的關聯,會收到 ActiveRecord::AssociationTypeMismatch

選項

可以使用選項調整所有關聯巨集。這讓比簡單和可預測的案例更加複雜的案例得以實現。

方法
B
H

執行個體公開方法

belongs_to(name, scope = nil, **options)

指定與另一個類別的一對一關聯。此方法僅在這個類別包含外來鍵時使用。如果另一個類別包含外來鍵,則應該使用 has_one 代替。請參閱 Is it a belongs_to or has_one association?,進一步了解 has_onebelongs_to 的使用時機。

方法將新增用於擷取並查詢關聯的單一物件,且這個物件包含 id

association 是用作傳遞作為 name 參數符號的保留標記位置,因此 belongs_to :author 會新增 author.nil? 及其他項目。

關聯

傳回關聯的物件。如果沒有找到物件,則傳回 nil

association=(associate)

指定關聯的物件,擷取主鍵並將其設為外來鍵。不會修改或刪除現有的記錄。

build_association(attributes = {})

傳回關聯類型的新的物件,該物件搭配 attributes 執行實例化,並透過外來鍵與這個物件進行連結,但尚未儲存。

create_association(attributes = {})

傳回關聯類型的新的物件,該物件搭配 attributes 執行實例化,透過外來鍵與這個物件進行連結,且已經儲存(如果驗證通過)。

create_association!(attributes = {})

執行與 create_association 相同的動作,但在記錄無效時會引發 ActiveRecord::RecordInvalid

reload_association

傳回關聯的物件,強制執行資料庫讀取。

reset_association

解除載入關聯的物件。下次存取會從資料庫中查詢該物件。

association_changed?

如果已經指定新的關聯物件,且接下來的儲存動作會更新外來鍵,則傳回 true。

association_previously_changed?

如果先前的儲存動作更新關聯,以參照新的關聯物件,則傳回 true。

範例

class Post < ActiveRecord::Base
  belongs_to :author
end

宣告 belongs_to :author 會新增以下方法 (及其他方法)

post = Post.find(7)
author = Author.find(19)

post.author           # similar to Author.find(post.author_id)
post.author = author  # similar to post.author_id = author.id
post.build_author     # similar to post.author = Author.new
post.create_author    # similar to post.author = Author.new; post.author.save; post.author
post.create_author!   # similar to post.author = Author.new; post.author.save!; post.author
post.reload_author
post.reset_author
post.author_changed?
post.author_previously_changed?

範圍

您可以傳遞第二個引數 scope 作為可呼叫項目 (也即過程或 lambda),以在存取關聯的物件時取得特定的記錄或自訂產生的查詢。

範圍範例

belongs_to :firm, -> { where(id: 2) }
belongs_to :user, -> { joins(:friends) }
belongs_to :level, ->(game) { where("game_level > ?", game.current_level) }

選項

宣告也可以包含 options hash,以指定關聯的行為類型。

:class_name

指定關聯的類別名稱。只有在無法從關聯名稱推論出名稱時才使用它。因此 belongs_to :author 預設會連結到 Author 類別,但如果實際的類別名稱為 Person,則必須使用此選項指定。

:foreign_key

指定用於關聯的外來鍵。預設情況下,這會猜認為關聯名稱加上「_id」後綴。因此,定義 belongs_to :person 關聯的類別會使用「person_id」為預設 :foreign_key。類似地,belongs_to :favorite_person, class_name: "Person" 會使用外來鍵「favorite_person_id」。

設定 :foreign_key 選項會防止自動偵測關聯的反向,因此通常最好同時設定 :inverse_of 選項。

:foreign_type

如果這是多形關聯,請指定用來儲存關聯物件類型的欄位,預設會將關聯名稱加上「_type」後綴作為此欄位名稱。因此,定義 belongs_to :taggable, polymorphic: true 關聯的類別會將「taggable_type」用作預設的 :foreign_type

:primary_key

指定用於取得關聯中使用的關聯物件主鍵的方法。預設為 id

:dependent

若設為 :destroy,關聯物件會在刪除這個物件時被刪除。若設為 :delete,關聯物件在呼叫其刪除方法時就會被刪除。若設為 :destroy_async,關聯物件會被排程在背景工作中刪除。由於可能會留下孤立的記錄,如果 belongs_to 另一個類別時與 has_many 關聯一起使用,就不應指定此選項。

:counter_cache

透過使用 CounterCache::ClassMethods#increment_counterCounterCache::ClassMethods#decrement_counter 快取關聯類別中屬於物件的數量。當此類別的物件建立時,會增加快取計數;當其被刪除時,則會減少快取計數。這需要在關聯類別(例如 Post 類別)中使用名為 #{table_name}_count 的欄位(例如對於屬於 Comment 類別,則是 comments_count) - 也就是要在關聯類別上建立 #{table_name}_count 的遷移(使 Post.comments_count 會傳回快取的計數)。您也可以透過提供欄位名稱(而非 true/false 值)此選項,來指定自訂快取計數欄位 (例如:counter_cache: :my_custom_counter)。

在現有的大型資料表上開始使用快取計數會很麻煩,因為在新增欄位時,必須分別填充欄位值(以免鎖定資料表時間太長),而且必須在使用 :counter_cache 之前填充(否則如 size/any?/等等在內部使用快取計數的方法,可能會產生不正確的結果)。若要安全地填入這些值,同時讓快取計數欄位與子記錄的建立/移除保持最新,並且避免上述方法使用可能不正確的快取計數欄位值,並始終從資料庫取得結果,請使用 counter_cache: { active: false }。如果您還需要指定自訂欄位名稱,請使用 counter_cache: { active: false, column: :my_custom_counter }

注意:如果您已啟用快取計數,可能需要將快取計數屬性新增到關聯類別的 attr_readonly 清單中(例如:class Post; attr_readonly :comments_count; end)。

:polymorphic

透過傳遞 true 來指定此關聯為多形關聯。注意:由於多形關聯仰賴在資料庫中儲存類別名稱,所以請務必在對應列的 *_type 多形類型欄位中更新類別名稱。

:validate

設為 true 時,在儲存父物件時會驗證新增至關聯的新物件。預設為 false。如果您要確保關聯物件在每次更新時都會重新驗證,請使用 validates_associated

:autosave

如果為 true,在儲存父物件時會永遠儲存關聯物件,或是在標記為刪除時將其刪除。如果為 false,則不會儲存或刪除關聯物件。預設只會在關聯物件是新記錄時將其儲存。

請注意,NestedAttributes::ClassMethods#accepts_nested_attributes_for 將設定 :autosavetrue

:touch

如果為 true,當此記錄儲存或銷毀時,關聯物件會被觸發 (將 updated_at / updated_on 屬性設定為目前時間)。如果指定符號,會將此屬性與 updated_at / updated_on 屬性一起更新為目前時間。請注意,觸發時不會執行驗證,而且只有 after_touchafter_commitafter_rollback 回呼會執行。

:inverse_of

指定關聯物件上 has_onehas_many 關聯的名稱,也就是此 belongs_to 關聯的反向。有關更多詳細資訊,請參閱 雙向關聯

:optional

設定為 true 時,此關聯不會驗證其存在。

:required

設定為 true 時,此關聯也會驗證其存在。這會驗證關聯本身,而不是驗證 id。你可以使用 :inverse_of 來避免驗證期間的額外查詢。注意:預設將 required 設定為 true,且此設定已棄用。如果您不想驗證關聯存在,請使用 optional: true

:default

提供一個可呼叫式物件 (例如程序或 lambda) 來指定在驗證前,關聯應使用特定記錄進行初始化。請注意,如果記錄存在,不會執行可呼叫式物件。

:strict_loading

每次透過這個關聯載入關聯記錄時,強制載入。勁量

:ensuring_owner_was

指定要在所有者上呼叫的實體方法。方法必須傳回 true,關聯記錄才會在背景工作刪除。

:query_constraints

會作為複合外來金鑰。定義用於查詢關聯物件的欄位清單。這是選用選項。預設情況下,Rails 會嘗試自動擷取值。設定值時,陣列 大小必須與關聯模型的主鍵或 query_constraints 大小相符。

選項範例

belongs_to :firm, foreign_key: "client_of"
belongs_to :person, primary_key: "name", foreign_key: "person_name"
belongs_to :author, class_name: "Person", foreign_key: "author_id"
belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
                          class_name: "Coupon", foreign_key: "coupon_id"
belongs_to :attachable, polymorphic: true
belongs_to :project, -> { readonly }
belongs_to :post, counter_cache: true
belongs_to :comment, touch: true
belongs_to :company, touch: :employees_last_updated_at
belongs_to :user, optional: true
belongs_to :account, default: -> { company.account }
belongs_to :account, strict_loading: true
belongs_to :note, query_constraints: [:organization_id, :note_id]
# File activerecord/lib/active_record/associations.rb, line 1689
def belongs_to(name, scope = nil, **options)
  reflection = Builder::BelongsTo.build(self, name, scope, options)
  Reflection.add_reflection self, name, reflection
end

has_and_belongs_to_many(name, scope = nil, **options, &extension)

指定與另一類別的多對多關係。透過中介的連結表格,這會將兩個類別串聯起來。除非明確地將連結表格指定為選項,否則系統會使用類別名稱的字典順序來猜測。因此,開發人員與專案之間的連結將會給出「developers_projects」的預設連結表格名稱,因為「D」在字母順序上出現在「P」前面。請注意這個優先次序是使用 < 算子來計算 String。這表示如果字串長度不同,且這兩個字串在比較到最短長度時相等,那麼較長的字串會被視為具有比較短的字串更高的字典優先次序。例如,你原本預期「paper_boxes」與「papers」表格會產生「papers_paper_boxes」的連結表格名稱,但實際上產生的連結表格名稱是「paper_boxes_papers」。請注意這個警告,若有需要,請使用自訂的 :join_table 選項。如果你的表格共用一個相同的前置詞,該前置詞只會出現在最前面一次。例如,「catalog_categories」與「catalog_products」表格會產生「catalog_categories_products」的連結表格名稱。

連結表格不應該具備主要金鑰,或與它關聯的模型。你必須手動產生連結表格,例如使用遷移

class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[8.0]
  def change
    create_join_table :developers, :projects
  end
end

在這些欄位中加入索引,以加快連結程序,這也是個好主意。然而,在 MySQL 中,建議針對這兩個欄位加入複合索引,因為 MySQL 在查詢期間只會對每個表格使用一個索引。

加入下列檢索與查詢方法

collection 是充當傳遞至 name 引數的符號的替代字,因此在 has_and_belongs_to_many :categories 中,會加入 categories.empty?

collection

傳回所有關聯物件的 Relation。如果未找到任何關聯物件,則會傳回空的 Relation

collection<<(object, ...)

透過在連結表格中 (collection.pushcollection.concat 是此方法的別名) 建立關聯,將一個或多個物件加入集合中。請注意,這個運作會立即啟動 update SQL,不會等到在父物件中儲存或呼叫更新,除非父物件是新的記錄。

collection.delete(object, ...)

透過從連結表格中移除它們的關聯,將一個或多個物件從集合中移除。這不會移除物件。

collection.destroy(object, ...)

透過對連結表格中的每個關聯執行移除,覆寫任何相關選項,將一個或多個物件從集合中移除。這不會移除物件。

collection=objects

透過適當地刪除與加入物件,取代集合的內容。

collection_singular_ids

傳回關聯物件的 ID 陣列。

collection_singular_ids=ids

使用 ids 中主要金鑰所識別出的物件取代集合。

collection.clear

從集合中移除每個物件。這不會移除物件。

collection.empty?

如果沒有關聯物件,則傳回 true

collection.size

傳回關聯物件的數目。

collection.find(id)

尋找與 id 相符,且符合它必須與這個物件關聯的條件的關聯物件。使用與 ActiveRecord::FinderMethods#find 相同的規則。

collection.exists?(...)

確認是否存在具有指定條件的關聯物件。使用與 ActiveRecord::FinderMethods#exists? 相同的規則。

collection.build(attributes = {})

傳回已使用 attributes 指示的集合類型新物件,並已透過連結表格連結至這個物件,但尚未儲存。

collection.create(attributes = {})

傳回已實例化的收集類型的新物件,透過連結表格連結至物件,並於儲存 (通過驗證) 後將儲存。

collection.reload

傳回強制資料庫讀取的關聯物件 Relation。找不到相關資料時,會傳回空的 Relation

範例

class Developer < ActiveRecord::Base
  has_and_belongs_to_many :projects
end

宣告 has_and_belongs_to_many :projects 將加入下列方法 (及更多)

developer = Developer.find(11)
project   = Project.find(9)

developer.projects
developer.projects << project
developer.projects.delete(project)
developer.projects.destroy(project)
developer.projects = [project]
developer.project_ids
developer.project_ids = [9]
developer.projects.clear
developer.projects.empty?
developer.projects.size
developer.projects.find(9)
developer.projects.exists?(9)
developer.projects.build  # similar to Project.new(developer_id: 11)
developer.projects.create # similar to Project.create(developer_id: 11)
developer.projects.reload

宣告中可能會包含一個 options hash,用來指定關聯行為。

範圍

您可以傳遞第二個引數 scope 作為可呼叫的函式 (即程序或 lambda),以便在存取關聯收集時,擷取特定組的記錄,或自訂產生的查詢。

範圍範例

has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
has_and_belongs_to_many :categories, ->(post) {
  where("default_category = ?", post.default_category)
}

延伸模組

extension 引數允許您傳遞區塊至 has_and_belongs_to_many 關聯中。這對於將新的尋找器、建立器及其他作為關聯一部分的工廠型態方法一併加入,非常有用。

延伸範例

has_and_belongs_to_many :contractors do
  def find_or_create_by_name(name)
    first_name, last_name = name.split(" ", 2)
    find_or_create_by(first_name: first_name, last_name: last_name)
  end
end

選項

:class_name

指定關聯的類別名稱。僅在無法從關聯名稱推斷得知名稱,才使用此選項。例如 has_and_belongs_to_many :projects 預設會連結至 Project 類別,但實際類別名稱為 SuperProject 的話,就必須使用此選項來指定名稱。

:join_table

指定連結表格名稱 (若不想使用預設的語彙順序)。警告:如果您覆寫了任一類別的表格名稱,那麼為了讓 table_name 方法能夠作用,必須在任何 has_and_belongs_to_many 宣告下方宣告該方法。

:foreign_key

指定用於關聯的外來金鑰。預設值會猜測為小寫此類別名稱,並加上「_id」後綴。因此,將 Person 類別與 Project 建立 has_and_belongs_to_many 關聯,則預設的 :foreign_key 為「person_id」。

設定 :foreign_key 選項會防止自動偵測關聯的反向,因此通常最好同時設定 :inverse_of 選項。

:association_foreign_key

指定用於關聯中接收端關聯的外來金鑰。預設值會猜測為小寫關聯類別名稱,並加上「_id」後綴。因此,若 Person 類別對 Project 建立 has_and_belongs_to_many 關聯,則預設的 :association_foreign_key 為「project_id」。

:validate

設為 true 時,會在儲存父物件時,驗證新增至關聯的新物件。預設值為 true。若希望確保關聯物件會在每次更新時重新驗證,請使用 validates_associated

:autosave

若為 true,儲存父物件時,會永遠儲存關聯物件,或在標示為毀損時毀損關聯物件。若為 false,則絕不儲存或毀損關聯物件。預設只儲存為新增記錄的關聯物件。

請注意,NestedAttributes::ClassMethods#accepts_nested_attributes_for 將設定 :autosavetrue

:strict_loading

強制在每次透過這個關聯載入關聯記錄時,進行嚴格載入。

選項範例

has_and_belongs_to_many :projects
has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
has_and_belongs_to_many :nations, class_name: "Country"
has_and_belongs_to_many :categories, join_table: "prods_cats"
has_and_belongs_to_many :categories, -> { readonly }
has_and_belongs_to_many :categories, strict_loading: true
# File activerecord/lib/active_record/associations.rb, line 1870
        def has_and_belongs_to_many(name, scope = nil, **options, &extension)
          habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)

          builder = Builder::HasAndBelongsToMany.new name, self, options

          join_model = builder.through_model

          const_set join_model.name, join_model
          private_constant join_model.name

          middle_reflection = builder.middle_reflection join_model

          Builder::HasMany.define_callbacks self, middle_reflection
          Reflection.add_reflection self, middle_reflection.name, middle_reflection
          middle_reflection.parent_reflection = habtm_reflection

          include Module.new {
            class_eval <<-RUBY, __FILE__, __LINE__ + 1
              def destroy_associations
                association(:#{middle_reflection.name}).delete_all(:delete_all)
                association(:#{name}).reset
                super
              end
            RUBY
          }

          hm_options = {}
          hm_options[:through] = middle_reflection.name
          hm_options[:source] = join_model.right_reflection.name

          [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend, :strict_loading].each do |k|
            hm_options[k] = options[k] if options.key? k
          end

          has_many name, scope, **hm_options, &extension
          _reflections[name].parent_reflection = habtm_reflection
        end

has_many(name, scope = nil, **options, &extension)

指定一對多關聯。下列用於擷取並查詢關聯物件收集的方法,會一併加入

collection 是作為名稱引數傳遞的符號之佔位符,例如 has_many :clients 會新增 clients.empty? 等方法。

collection

傳回所有關聯物件的 Relation。如果未找到任何關聯物件,則會傳回空的 Relation

collection<<(object, ...)

將一個或多個物件加入集合中,藉由設定其外鍵至集合中的主鍵。請注意,除非母物件為新記錄,否則此操作即刻執行更新 SQL,而不需等到母物件的儲存或更新呼叫。這也會執行關聯物件的驗證和回呼。

collection.delete(object, ...)

將一個或多個物件從集合中移除,藉由設定其外鍵為 NULL。物件除了會被摧毀,如果其與 dependent: :destroy 有關聯,也會被刪除,如果其與 dependent: :delete_all 有關聯。

如果使用 :through 選項,則預設會刪除 (而非將其設為空值) 連接記錄,但您可以指定 dependent: :destroydependent: :nullify 來覆寫此設定。

collection.destroy(object, ...)

將一個或多個物件從集合中移除,藉由執行 destroy 在每條記錄上,不考慮任何從屬選項,確保回呼會執行。

如果使用 :through 選項,則會摧毀連接記錄,而不是物件本身。

collection=objects

適當地刪除並新增物件,取代集合內容。如果 :through 選項為 true,則連接模型中的回呼會被觸發,但摧毀回呼除外,因為預設會直接刪除。您可以指定 dependent: :destroydependent: :nullify 來覆寫此設定。

collection_singular_ids

傳回關聯物件之識別碼的陣列

collection_singular_ids=ids

ids 中主鍵所識別的物件取代集合。此方法會載入模型並呼叫 collection=。請參閱上方說明。

collection.clear

將集合中的每個物件移除。這會摧毀與 dependent: :destroy 有關聯的物件,如果與 dependent: :delete_all 關聯,直接從資料庫中刪除,否則將其外鍵設定為 NULL。如果 :through 選項為 true,不會在連接模型中呼叫任何摧毀回呼。連接模型會直接被刪除。

collection.empty?

如果沒有關聯物件,則傳回 true

collection.size

傳回關聯物件的數目。

collection.find(...)

依據與 ActiveRecord::FinderMethods#find 相同的規則找出關聯物件。

collection.exists?(...)

確認是否存在具有指定條件的關聯物件。使用與 ActiveRecord::FinderMethods#exists? 相同的規則。

collection.build(attributes = {}, ...)

傳回集合類型中一個或多個新的物件,已使用 attributes 執行實例化,並透過外鍵連結到此物件,但尚未儲存。

collection.create(attributes = {})

傳回集合類型中的新物件,已使用 attributes 執行實例化,透過外鍵連結到此物件,並且已經儲存 (如果它通過驗證)。注意:這只適用於資料庫中已存在的基礎模型,不適用於新的 (未儲存的) 記錄!

collection.create!(attributes = {})

執行與 collection.create 相同的動作,但如果記錄無效會觸發 ActiveRecord::RecordInvalid

collection.reload

傳回強制資料庫讀取的關聯物件 Relation。找不到相關資料時,會傳回空的 Relation

範例

class Firm < ActiveRecord::Base
  has_many :clients
end

宣告 has_many :clients 會新增以下方法 (以及更多)

firm = Firm.find(2)
client = Client.find(6)

firm.clients                       # similar to Client.where(firm_id: 2)
firm.clients << client
firm.clients.delete(client)
firm.clients.destroy(client)
firm.clients = [client]
firm.client_ids
firm.client_ids = [6]
firm.clients.clear
firm.clients.empty?                # similar to firm.clients.size == 0
firm.clients.size                  # similar to Client.count "firm_id = 2"
firm.clients.find                  # similar to Client.where(firm_id: 2).find(6)
firm.clients.exists?(name: 'ACME') # similar to Client.exists?(name: 'ACME', firm_id: 2)
firm.clients.build                 # similar to Client.new(firm_id: 2)
firm.clients.create                # similar to Client.create(firm_id: 2)
firm.clients.create!               # similar to Client.create!(firm_id: 2)
firm.clients.reload

宣告也可以包含 options hash,以指定關聯的行為類型。

範圍

您可以傳遞第二個引數 scope 作為可呼叫的函式 (即程序或 lambda),以便在存取關聯收集時,擷取特定組的記錄,或自訂產生的查詢。

範圍範例

has_many :comments, -> { where(author_id: 1) }
has_many :employees, -> { joins(:address) }
has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) }

延伸模組

extension 參數允許您將區塊傳遞到 has_many 關聯。這對於新增新的查詢器、建立器和其他工廠類型的用於該關聯的方法很有用。

延伸範例

has_many :employees do
  def find_or_create_by_name(name)
    first_name, last_name = name.split(" ", 2)
    find_or_create_by(first_name: first_name, last_name: last_name)
  end
end

選項

:class_name

指定關聯的類別名稱。只有在無法從關聯名稱推論出時才用得到。因此,預設 has_many :products 將連結到 Product 類別,但如果真正的類別名稱為 SpecialProduct,您必須使用此選項指定它。

:foreign_key

指定關聯中使用的外來鍵。預設會猜測這會是小寫的類別名稱,並附加「_id」。因此,建立 has_many 關聯的 Person 類別會將「person_id」當作預設的 :foreign_key

設定 :foreign_key 選項會防止自動偵測關聯的反向,因此通常最好同時設定 :inverse_of 選項。

:foreign_type

如果這是多型關聯,請指定用於儲存關聯物件類型的欄位。預設會猜測這是「as」選項中指定的「多型關聯」的名稱,並附加「_type」字尾。因此,定義 has_many :tags, as: :taggable 關聯的類別會將「taggable_type」當作預設的 :foreign_type

:primary_key

指定用於關聯的主鍵的欄位名稱。預設為 id

:dependent

控制當擁有者物件被刪除時會對關聯物件發生什麼事。請注意,這些都是以 callback 實作,而 Rails 會依序執行 callback。因此,其他類似的 callback 可能會影響 :dependent 行為,而 :dependent 行為可能會影響其他 callback。

  • nil 不執行任何動作(預設)。

  • :destroy 會導致所有關聯物件也會被刪除。

  • :destroy_async 會在背景作業中刪除所有關聯物件。警告:如果關聯有資料庫外來鍵約束,請不要使用此選項。外來鍵約束動作會在與刪除其擁有者相同的交易中進行。

  • :delete_all 會導致所有關聯物件直接從資料庫中刪除(因此不會執行 callback)。

  • :nullify 會將外來鍵設定為 NULL。多型關聯的多型類型也會變為無效。不會執行 Callback

  • :restrict_with_exception 會在有任何關聯記錄時引發 ActiveRecord::DeleteRestrictionError 例外。

  • :restrict_with_error 會在有任何關聯物件時將錯誤新增到擁有者。

如果與 :through 選項一起使用,則連結模式上的關聯必須是 belongs_to,而且會被刪除的記錄是連結記錄,而不是關聯記錄。

如果在範圍關聯上使用 dependent: :destroy,則只會刪除範圍物件。例如,如果 Post 模型定義 has_many :comments, -> { where published: true }, dependent: :destroy ,而且對某個貼文呼叫 destroy,則只會刪除已發表的留言。這表示資料庫中的任何未發表的留言仍會包含指向現在已刪除貼文的外來鍵。

:counter_cache

此選項可以用於設定自訂名稱的 :counter_cache. 您只在於 belongs_to 關聯上自訂 :counter_cache 名稱時才需要這個選項。

:as

指定多型介面(請參閱 belongs_to)。

:through

指定要透過哪個關聯來執行查詢。這可以是任何其他類型的關聯,包括其他 :through 關聯。:class_name:primary_key:foreign_key 的選項會被忽略,因為關聯會使用來源反映。

如果 join 模型中的關聯是 belongs_to,那麼集合可以修改,並且 :through 模型中的記錄會根據實際情況自動新增或移除。否則集合為唯讀,因此你應該直接控制 :through 關聯。

如果你想要修改關聯(而不是僅從中讀取),那麼建議在 join 模型的來源關聯上設定 :inverse_of 選項。這樣可以建構關聯記錄,而且當這些記錄儲存後,會自動產生適當的 join 模型記錄。詳情請參閱 關聯加入模型設定反向

:disable_joins

指定是否要在關聯中略過加入。如果設為 true,將會產生多個查詢。請注意,在某些情況下,如果套用排序或限制,則會因為資料庫限制而在記憶體中進行該作業。此選項僅適用於 has_many :through 關聯,因為單獨使用 has_many 時不會執行加入。

:source

指定 has_many :through 查詢所使用的來源關聯名稱。僅在無法從關聯中推斷名稱時使用。has_many :subscribers, through: :subscriptions 會在 Subscription 中尋找 :subscribers:subscriber,除非有提供 :source

:source_type

指定 has_many :through 查詢所使用的來源關聯類型,其中來源關聯是多型態 belongs_to

:validate

設為 true 時,會在儲存父物件時,驗證新增至關聯的新物件。預設值為 true。若希望確保關聯物件會在每次更新時重新驗證,請使用 validates_associated

:autosave

如果為 true,則儲存上層物件時,總會儲存關聯物件或在標註為要毀損時毀損它們。如果為 false,則絕不會儲存或毀損關聯物件。預設情況下,只會儲存新的記錄的關聯物件。此選項實作成 before_save 回呼。由於回呼會依定義順序執行,因此可能需要在使用者定義的任何 before_save 回呼中明確儲存關聯物件。

請注意,NestedAttributes::ClassMethods#accepts_nested_attributes_for 將設定 :autosavetrue

:inverse_of

指定 belongs_to 關聯的名稱,該關聯在關聯物件上,而且是此 has_many 關聯的反向。詳情請參閱 雙向關聯

:extend

指定一個模組或模組陣列,會擴充到傳回的關聯物件中。可用於定義關聯的方法,特別是在它們應該在多個關聯物件之間共用時。

:strict_loading

當設為 true 時,會強制在每次透過此關聯載入關聯記錄時進行嚴格載入。

:ensuring_owner_was

指定要在所有者上呼叫的實體方法。方法必須傳回 true,關聯記錄才會在背景工作刪除。

:query_constraints

會作為複合外來金鑰。定義用於查詢關聯物件的欄位清單。這是選用選項。預設情況下,Rails 會嘗試自動擷取值。設定值時,陣列 大小必須與關聯模型的主鍵或 query_constraints 大小相符。

:index_errors

允許區分關聯記錄中的多個驗證錯誤,方法是在錯誤屬性名稱中包含索引,例如 roles[2].level。當設為 true 時,索引會根據關聯順序(也就是資料庫順序)進行,包括置於最後的尚未持續存在的新記錄。當設為 :nested_attributes_order 時,索引會根據接受 nested attributes for 時巢狀屬性設定器所接收的記錄順序進行。

:before_add

定義一個 關聯回呼,這個回呼會在物件新增到關聯集合之前觸發。

:after_add

定義一個 關聯回呼,這個回呼會在物件新增到關聯集合之後觸發。

:before_remove

定義一個 關聯回呼,這個回呼會在物件移除到關聯集合之前觸發。

:after_remove

定義 關聯回呼,當一個物件從關聯集合中移除掉時執行。

選項範例

has_many :comments, -> { order("posted_on") }
has_many :comments, -> { includes(:author) }
has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
has_many :tracks, -> { order("position") }, dependent: :destroy
has_many :comments, dependent: :nullify
has_many :tags, as: :taggable
has_many :reports, -> { readonly }
has_many :subscribers, through: :subscriptions, source: :user
has_many :subscribers, through: :subscriptions, disable_joins: true
has_many :comments, strict_loading: true
has_many :comments, query_constraints: [:blog_id, :post_id]
has_many :comments, index_errors: :nested_attributes_order
# File activerecord/lib/active_record/associations.rb, line 1302
def has_many(name, scope = nil, **options, &extension)
  reflection = Builder::HasMany.build(self, name, scope, options, &extension)
  Reflection.add_reflection self, name, reflection
end

has_one(name, scope = nil, **options)

指定一對一與另一類別的關聯。只有當另一類別包含外來鍵時才應使用此方法。如果目前的類別包含外來鍵,則您應使用 belongs_to。請參閱 Is it a belongs_to or has_one association? 以取得使用 has_onebelongs_to 時機的更多資訊。

將加入以下方法,以擷取及查詢單一關聯物件

association 是傳遞為 name 引數的符號的佔位符,因此 has_one :manager 將在其他方法中新增 manager.nil?

關聯

傳回關聯的物件。如果沒有找到物件,則傳回 nil

association=(associate)

指派關聯物件、擷取主鍵、將其設定為外來鍵並儲存關聯物件。為了避免資料庫不一致,當指派新的關聯物件時,永久刪除現存關聯物件,即使新的關聯物件未儲存至資料庫中。

build_association(attributes = {})

傳回關聯類型的新的物件,該物件搭配 attributes 執行實例化,並透過外來鍵與這個物件進行連結,但尚未儲存。

create_association(attributes = {})

傳回關聯類型的新的物件,該物件搭配 attributes 執行實例化,透過外來鍵與這個物件進行連結,且已經儲存(如果驗證通過)。

create_association!(attributes = {})

執行與 create_association 相同的動作,但在記錄無效時會引發 ActiveRecord::RecordInvalid

reload_association

傳回關聯的物件,強制執行資料庫讀取。

reset_association

解除載入關聯的物件。下次存取會從資料庫中查詢該物件。

範例

class Account < ActiveRecord::Base
  has_one :beneficiary
end

宣告 has_one :beneficiary 將新增以下方法(以及更多方法)

account = Account.find(5)
beneficiary = Beneficiary.find(8)

account.beneficiary               # similar to Beneficiary.find_by(account_id: 5)
account.beneficiary = beneficiary # similar to beneficiary.update(account_id: 5)
account.build_beneficiary         # similar to Beneficiary.new(account_id: 5)
account.create_beneficiary        # similar to Beneficiary.create(account_id: 5)
account.create_beneficiary!       # similar to Beneficiary.create!(account_id: 5)
account.reload_beneficiary
account.reset_beneficiary

範圍

您可以傳遞第二個引數 scope 作為可呼叫項目 (也即過程或 lambda),以在存取關聯的物件時取得特定的記錄或自訂產生的查詢。

範圍範例

has_one :author, -> { where(comment_id: 1) }
has_one :employer, -> { joins(:company) }
has_one :latest_post, ->(blog) { where("created_at > ?", blog.enabled_at) }

選項

宣告也可以包含 options hash,以指定關聯的行為類型。

選項如下

:class_name

指定關聯的類別名稱。僅在無法從關聯名稱推論此名稱時使用。因此 has_one :manager 在預設情況下將連結至經理類別,但如果實際類別名稱為個人,您必須使用此選項指定。

:dependent

控制在關聯的擁有者被銷毀時關聯物件發生什麼事

  • nil 不執行任何動作(預設)。

  • :destroy 會導致關聯物件也一併銷毀

  • :destroy_async 會導致關聯物件在背景工作中銷毀。警告:如果關聯在資料庫中受外來鍵約束限制,請勿使用此選項。外來鍵約束動作會在刪除其擁有者的同一個交易中執行。

  • :delete 會導致關聯物件直接從資料庫中刪除(因此不會執行回呼)

  • :nullify 會將外來鍵設定為 NULL。多形類型欄位在多形關聯中也會被設定為 NULL。不會執行 回呼

  • :restrict_with_exception 會在有相關記錄時引發 ActiveRecord::DeleteRestrictionError 例外

  • :restrict_with_error 會在有相關物件時新增錯誤至擁有者

請注意,在使用 :through 選項時會忽略 :dependent 選項。

:foreign_key

指定用於關聯的外來鍵。預設會猜測為小寫的此類別名稱,並附加上字尾 “_id”。因此,建立 has_one 關聯的個人類別會使用 “person_id” 作為預設 :foreign_key

設定 :foreign_key 選項會防止自動偵測關聯的反向,因此通常最好同時設定 :inverse_of 選項。

:foreign_type

如果這是一個多形關聯,則指定用於儲存關聯物件類型的欄位。預設會猜測為在 “as” 選項中指定的關聯名稱,並附加上字尾 “_type”。因此,定義 has_one :tag, as: :taggable 關聯的類別會使用 “taggable_type” 作為預設 :foreign_type

:primary_key

指定用於傳回用於關聯的主鍵的方法。預設為 id

:as

指定多型介面(請參閱 belongs_to)。

:through

指定用於執行查詢的連接模型。由於關聯會使用來源反映,因此 :class_name:primary_key:foreign_key 選項皆被忽略。您只能透過連接模型對 has_onebelongs_to 關聯使用 :through 查詢。

如果 join 模型中的關聯是 belongs_to,那麼集合可以修改,並且 :through 模型中的記錄會根據實際情況自動新增或移除。否則集合為唯讀,因此你應該直接控制 :through 關聯。

如果你想要修改關聯(而不是僅從中讀取),那麼建議在 join 模型的來源關聯上設定 :inverse_of 選項。這樣可以建構關聯記錄,而且當這些記錄儲存後,會自動產生適當的 join 模型記錄。詳情請參閱 關聯加入模型設定反向

:disable_joins

指定是否要略過關聯的聯結。若設為 true,將會產生兩個以上的查詢。請注意,有時若套用順序或限制,將會因資料庫限制而在記憶體中進行。這個選項僅適用於 has_one :through 關聯,因為 has_one 本身不會執行聯結。

:source

指定 has_one :through 查詢使用的來源關聯名稱。僅在無法根據關聯推出名稱時使用。has_one :favorite, through: :favorites 將會尋找 Favorite 上的 :favorite,除非已提供 :source

:source_type

指定 has_one :through 查詢使用的來源關聯類型,其中來源關聯為多型態的 belongs_to

:validate

設為 true 時,在儲存父物件時會驗證新增至關聯的新物件。預設為 false。如果您要確保關聯物件在每次更新時都會重新驗證,請使用 validates_associated

:autosave

如果為 true,在儲存父物件時,請務必儲存關聯物件或在標記為要銷毀時銷毀關聯物件。如果為 false,請勿儲存或銷毀關聯物件。

預設情況下,僅在關聯物件是新記錄時才儲存。將這個選項設定為 true 時,除非使用 validate: false 明確停用,否則也會對關聯物件啟用驗證。這是因為儲存具有無效關聯物件的物件將會失敗,因此任何關聯物件都會歷經驗證檢查。

請注意,NestedAttributes::ClassMethods#accepts_nested_attributes_for 將設定 :autosavetrue

:touch

如果為 true,當此記錄儲存或銷毀時,關聯物件會被觸發 (將 updated_at / updated_on 屬性設定為目前時間)。如果指定符號,會將此屬性與 updated_at / updated_on 屬性一起更新為目前時間。請注意,觸發時不會執行驗證,而且只有 after_touchafter_commitafter_rollback 回呼會執行。

:inverse_of

指定關聯物件上 belongs_to 關聯的名稱,其為這個 has_one 關聯的反向。請參閱 雙向關聯 以取得更多詳細資料。

:required

將這個設定設為 true 時,關聯也會經過存在驗證。這會驗證關聯本身,而非 ID。您可以使用 :inverse_of 來避免在驗證期間執行額外的查詢。

:strict_loading

每次透過這個關聯載入關聯記錄時,強制載入。勁量

:ensuring_owner_was

指定要在所有者上呼叫的實體方法。方法必須傳回 true,關聯記錄才會在背景工作刪除。

:query_constraints

會作為複合外來金鑰。定義用於查詢關聯物件的欄位清單。這是選用選項。預設情況下,Rails 會嘗試自動擷取值。設定值時,陣列 大小必須與關聯模型的主鍵或 query_constraints 大小相符。

選項範例

has_one :credit_card, dependent: :destroy  # destroys the associated credit card
has_one :credit_card, dependent: :nullify  # updates the associated records foreign
                                              # key value to NULL rather than destroying it
has_one :last_comment, -> { order('posted_on') }, class_name: "Comment"
has_one :project_manager, -> { where(role: 'project_manager') }, class_name: "Person"
has_one :attachment, as: :attachable
has_one :boss, -> { readonly }
has_one :club, through: :membership
has_one :club, through: :membership, disable_joins: true
has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
has_one :credit_card, required: true
has_one :credit_card, strict_loading: true
has_one :employment_record_book, query_constraints: [:organization_id, :employee_id]
# File activerecord/lib/active_record/associations.rb, line 1498
def has_one(name, scope = nil, **options)
  reflection = Builder::HasOne.build(self, name, scope, options)
  Reflection.add_reflection self, name, reflection
end