跳至內容 跳至搜尋

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 關聯可用於描述模型之間的一對一、一對多和多對多的關係。每個模型使用關聯來描述其在關係中的角色。belongs_to 關聯總是使用在具有外來鍵的模型中。

一對一

在基礎模型中使用 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

多對多

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

第一種方式使用 has_many 關聯與 :through 選項和一個連接模型,因此有兩個關聯階段。

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(record)
    # ...
  end

  def log_after_remove(record)
    # ...
  end
end

你可以將回呼傳遞為陣列,以堆疊回呼。範例

class Firm < ActiveRecord::Base
  has_many :clients,
           dependent: :destroy,
           after_add: [:congratulate_client, -> (firm, record) { firm.log << "after_adding#{record.id}" }],
           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這樣的函式。

關聯加入模型

Has Many 關聯可以使用 :through 選項設定,以使用明確的加入模型來擷取資料。這與 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

這透過使用類型欄位加上外來金鑰來指定關聯記錄來運作。在資產範例中,您需要一個 attachable_id 整數欄位和一個 attachable_type 字串欄位。

將多型關聯與單一表格繼承 (STI) 結合使用有點棘手。為了讓關聯如預期般運作,請確保您將 STI 模型的基礎模型儲存在多型關聯的類型欄位中。若要繼續上述的資產範例,假設有使用文章表格進行 STI 的訪客文章和會員文章。在這種情況下,文章表格中必須有一個 type 欄位。

注意:在指定 attachable 時會呼叫 attachable_type= 方法。attachableclass_name 會傳遞為 字串

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|

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

我們可以使用下列方式在尋找器中參照兩個關聯,進一步改善情況

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

這會透過單一查詢載入所有留言。這會將總查詢次數減少到 3。一般而言,查詢次數會是 1 加上所命名關聯的數量(除非某些關聯是多型的 belongs_to - 請見下方)。

若要包含關聯的深度階層,請使用雜湊

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

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

所有這些功能不應讓您誤以為您可以提取大量資料而不會產生效能損失,只因為您減少了查詢次數。資料庫仍然需要將所有資料傳送給 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 個查詢。要載入的可尋址類型清單會根據載入的地址的背面來決定。如果 Active Record 必須回到熱切載入的先前實作,則不支援此功能,並且會引發 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 方法,以便執行回呼。然而,deletedelete_all 會根據 :dependent 選項指定的策略執行刪除,或者如果未提供 :dependent 選項,則會遵循預設策略。預設策略是不執行任何操作(讓外來鍵設定為父項識別碼),但 has_many :through 除外,其預設策略為 delete_all(刪除連接記錄,而不執行其回呼)。

還有一個方法 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。另請參閱 ActiveRecord::Associations::ClassMethods 關於何時使用 has_one 以及何時使用 belongs_to 的概觀。

將新增方法,用於擷取和查詢單一關聯物件,此物件有一個 id

association 是作為 name 引數傳遞的符號的佔位符,因此 belongs_to :author 會新增 author.nil? 等。

association

傳回關聯物件。如果找不到,則傳回 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)。註解:指定快取計數器會使用 attr_readonly 將其新增至該模型的唯讀屬性清單中。

:polymorphic

透過傳遞 true 來指定此關聯是多型關聯。註解:如果您已啟用快取計數器,您可能想要將快取計數器屬性新增至關聯類別中的 attr_readonly 清單(例如,class Post; attr_readonly :comments_count; end)。

:validate

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

:autosave

如果為 true,在儲存父物件時,總是儲存關聯物件,或是在標記為毀損時毀損關聯物件。如果為 false,則永遠不儲存或毀損關聯物件。預設情況下,只有在關聯物件是新記錄時才儲存關聯物件。

請注意,NestedAttributes::ClassMethods#accepts_nested_attributes_for 會將 :autosave 設定為 true

:touch

如果為 true,當此記錄儲存或毀損時,關聯物件會被觸發(updated_at / updated_on 屬性設定為目前時間)。如果您指定符號,除了 updated_at / updated_on 屬性之外,該屬性也會使用目前時間更新。請注意,觸發時不會執行任何驗證,而且只會執行 after_touchafter_commitafter_rollback 回呼。

:inverse_of

指定關聯物件上 has_onehas_many 關聯的 name,此關聯是此 belongs_to 關聯的相反關聯。有關更多詳細資訊,請參閱 ActiveRecord::Associations::ClassMethods 中雙向關聯的概觀。

:optional

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

:required

設為 true 時,關聯也會驗證其存在性。這會驗證關聯本身,而非 id。您可以使用 :inverse_of 來避免驗證期間的額外查詢。注意:required 預設設為 true,且已標示為不建議使用。如果您不希望驗證關聯存在性,請使用 optional: true

:default

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

:strict_loading

每次透過此關聯載入關聯記錄時,強制執行嚴格載入。

:ensuring_owner_was

指定要在擁有者上呼叫的執行個體方法。此方法必須傳回 true,才能在背景工作中刪除關聯記錄。

:query_constraints

用作複合外來金鑰。定義用於查詢關聯物件的欄位清單。這是一個選用選項。預設情況下,Rails 會嘗試自動衍生值。設定值時,Array 大小必須與關聯模型的主鍵或 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
belong_to  :note, query_constraints: [:organization_id, :note_id]
# File activerecord/lib/active_record/associations.rb, line 1886
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)

指定與其他類別的多對多關係。這會透過中間的聯結表格來關聯兩個類別。除非聯結表格明確指定為選項,否則會使用類別名稱的字典順序來猜測。因此,Developer 和 Project 之間的聯結會產生預設的聯結表格名稱「developers_projects」,因為「D」在字母順序中在「P」之前。請注意,此優先順序是使用 String< 算子計算的。這表示如果字串長度不同,且在比較到最短長度時字串相等,則較長的字串會被視為字典順序高於較短的字串。例如,由於「paper_boxes」名稱的長度,我們會預期表格「paper_boxes」和「papers」會產生聯結表格名稱「papers_paper_boxes」,但實際上它會產生聯結表格名稱「paper_boxes_papers」。請注意此警告,並在需要時使用自訂的 :join_table 選項。如果你的表格共用一個常見的前綴,它只會出現在開頭一次。例如,表格「catalog_categories」和「catalog_products」會產生聯結表格名稱「catalog_categories_products」。

聯結表格不應該有主鍵或與之關聯的模型。你必須使用遷移手動產生聯結表格,如下所示

class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[7.1]
  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 是此方法的別名)。請注意,此操作會立即觸發更新 SQL,而不會等到對父物件進行儲存或更新呼叫,除非父物件是新的記錄。

collection.delete(object, ...)

透過從聯結表格中移除其關聯,從集合中移除一個或多個物件。這不會刪除物件。

collection.destroy(object, ...)

透過在關聯表中的每個關聯執行 destroy,移除一個或多個物件,覆寫任何依賴選項。這不會摧毀物件。

collection=objects

透過刪除和新增物件來取代 collection 的內容。

collection_singular_ids

傳回關聯物件的 id 陣列。

collection_singular_ids=ids

使用 ids 中的主鍵來取代 collection 的物件。

collection.clear

從 collection 中移除每個物件。這不會摧毀物件。

collection.empty?

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

collection.size

傳回關聯物件的數量。

collection.find(id)

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

collection.exists?(...)

檢查是否存在符合給定條件的關聯物件。使用與 ActiveRecord::FinderMethods#exists? 相同的規則。

collection.build(attributes = {})

傳回已使用 attributes 執行實例化,並透過關聯表連結到這個物件,但尚未儲存的 collection 類型的新物件。

collection.create(attributes = {})

傳回已使用 attributes 執行實例化,透過關聯表連結到這個物件,且已儲存(如果通過驗證)的 collection 類型的新物件。

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)來擷取特定記錄集或自訂在存取關聯 collection 時產生的查詢。

範圍範例

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 類別,並建立一個 has_and_belongs_to_many 關聯到 Project,預設會使用「person_id」作為 :foreign_key

設定 :foreign_key 選項會防止自動偵測關聯的反向,因此通常建議也設定 :inverse_of 選項。

:association_foreign_key

指定關聯在接收端使用的外來鍵。預設會猜測為小寫的關聯類別名稱,並加上「_id」字尾。因此,如果建立一個 Person 類別,並建立一個 has_and_belongs_to_many 關聯到 Project,預設會使用「project_id」作為 :association_foreign_key

:validate

設定為 true 時,儲存父物件時會驗證新增到關聯的新物件。預設為 true。如果您希望在每次更新時都重新驗證關聯物件,請使用 validates_associated

:autosave

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

請注意,NestedAttributes::ClassMethods#accepts_nested_attributes_for 會將 :autosave 設定為 true

: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 2067
        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.to_s].parent_reflection = habtm_reflection
        end

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

指定一對多的關聯。將新增下列方法,用於擷取和查詢關聯物件的集合

collection 是作為 name 引數傳遞的符號的佔位符,因此 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)來擷取特定記錄集或自訂在存取關聯 collection 時產生的查詢。

範圍範例

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

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

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

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

  • :destroy_async 會在背景工作中刪除所有關聯物件。警告:如果關聯在資料庫中由外來金鑰約束所支援,請勿使用此選項。外來金鑰約束動作會在刪除其擁有者的同一個交易中發生。

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

  • :nullify 會將外來金鑰設定為 NULL。多型關聯的多型類型也會被設定為 NULL。不會執行 回呼

  • :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 的選項會被忽略,因為關聯使用來源反映。

如果關聯模型上的關聯是 belongs_to,則可以修改集合,而 :through 模型上的記錄會在適當時自動建立和移除。否則,集合是唯讀的,因此您應該直接操作 :through 關聯。

如果您要修改關聯 (而不是只從中讀取),則建議在關聯模型上設定來源關聯的 :inverse_of 選項。這允許建立關聯記錄,這些記錄在儲存時會自動建立適當的關聯模型記錄。(請參閱上述「關聯關聯模型」和「設定反向」區段。)

: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 會將 :autosave 設定為 true

:inverse_of

指定關聯物件上 belongs_to 關聯的名稱,該關聯是此 has_many 關聯的反向。有關更多詳細資訊,請參閱 ActiveRecord::Associations::ClassMethods 中關於雙向關聯的概觀。

:extend

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

:strict_loading

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

:ensuring_owner_was

指定要在擁有者上呼叫的執行個體方法。此方法必須傳回 true,才能在背景工作中刪除關聯記錄。

:query_constraints

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

選項範例

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]
# File activerecord/lib/active_record/associations.rb, line 1522
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。另請參閱 ActiveRecord::Associations::ClassMethods 中關於何時使用 has_one 以及何時使用 belongs_to 的概觀。

將新增下列用於擷取和查詢單一關聯物件的方法

association 是傳遞為 name 參數的符號的佔位符,因此 has_one :manager 會新增 manager.nil? 等方法。

association

傳回關聯物件。如果找不到,則傳回 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 預設會連結到 Manager 類別,但如果實際類別名稱為 Person,則必須使用此選項指定。

: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 類別會使用「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 查詢。

如果關聯模型上的關聯是 belongs_to,則可以修改集合,而 :through 模型上的記錄會在適當時自動建立和移除。否則,集合是唯讀的,因此您應該直接操作 :through 關聯。

如果您要修改關聯 (而不是只從中讀取),則建議在關聯模型上設定來源關聯的 :inverse_of 選項。這允許建立關聯記錄,這些記錄在儲存時會自動建立適當的關聯模型記錄。(請參閱上述「關聯關聯模型」和「設定反向」區段。)

: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,則永遠不儲存或毀損關聯物件。預設情況下,只有在關聯物件是新記錄時才儲存關聯物件。

請注意,NestedAttributes::ClassMethods#accepts_nested_attributes_for 會將 :autosave 設定為 true

:touch

如果為 true,當此記錄儲存或毀損時,關聯物件會被觸發(updated_at / updated_on 屬性設定為目前時間)。如果您指定符號,除了 updated_at / updated_on 屬性之外,該屬性也會使用目前時間更新。請注意,觸發時不會執行任何驗證,而且只會執行 after_touchafter_commitafter_rollback 回呼。

:inverse_of

指定關聯物件上 belongs_to 關聯的名稱,該關聯是此 has_one 關聯的反向。有關更多詳細資訊,請參閱 ActiveRecord::Associations::ClassMethods 中關於雙向關聯的概觀。

:required

設為 true 時,關聯也會驗證其存在性。這將驗證關聯本身,而非 id。您可以使用 :inverse_of 來避免驗證期間的額外查詢。

:strict_loading

每次透過此關聯載入關聯記錄時,強制執行嚴格載入。

:ensuring_owner_was

指定要在擁有者上呼叫的執行個體方法。此方法必須傳回 true,才能在背景工作中刪除關聯記錄。

:query_constraints

用作複合外來金鑰。定義用於查詢關聯物件的欄位清單。這是一個選用選項。預設情況下,Rails 會嘗試自動衍生值。設定值時,Array 大小必須與關聯模型的主鍵或 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 1708
def has_one(name, scope = nil, **options)
  reflection = Builder::HasOne.build(self, name, scope, options)
  Reflection.add_reflection self, name, reflection
end