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
繼承的方法,並會破壞某些功能。例如,attributes
和 connection
會是關聯名稱的不良選擇,因為這些名稱已存在於 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_one
、belongs_to
、has_many
或 has_and_belongs_to_many
關聯中設定 :autosave
選項。將其設定為 true
將總是儲存成員,而將其設定為 false
將永遠不會儲存成員。關於 :autosave
選項的更多詳細資訊可以在 AutosaveAssociation
中找到。
一對一關聯
-
將物件指定給
has_one
關聯會自動儲存該物件和被取代的物件(如果有),以更新它們的外來鍵 - 除非父物件未儲存(new_record? == true
)。 -
如果這些儲存有任何一個失敗(由於物件之一無效),則會引發
ActiveRecord::RecordNotSaved
例外,並且指定會被取消。 -
如果您希望將物件指定給
has_one
關聯而不儲存它,請使用#build_association
方法(如下所述)。被取代的物件仍會儲存以更新其外來鍵。 -
將物件指定給
belongs_to
關聯不會儲存物件,因為外來鍵欄位屬於父物件。它也不會儲存父物件。
集合
-
將物件新增至集合(
has_many
或has_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_add
、after_add
、before_remove
和 after_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_to
和has_one
的關聯物件,或傳回has_many
和has_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_one
或 has_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
選項,表示下列範例可以正確執行(其中 tags
是 has_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_many
、has_one
和 belongs_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
,將無法判斷如何設定中間的 Post
和 Comment
物件。
多型關聯
模型的多型關聯不受限於可以關聯的模型類型。相反地,它們會指定 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=
方法。attachable
的 class_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.id
和 LEFT 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
實例 d
和 t.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_many
、has_one
和 belongs_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_many
和 has_and_belongs_to_many
關聯具有方法 destroy
、delete
、destroy_all
和 delete_all
。
對於 has_and_belongs_to_many
,delete
和 destroy
相同:它們會導致移除連接資料表中的記錄。
對於 has_many
,destroy
和 destroy_all
將始終呼叫要移除記錄的 destroy
方法,以便執行回呼。然而,delete
和 delete_all
會根據 :dependent
選項指定的策略執行刪除,或者如果未提供 :dependent
選項,則會遵循預設策略。預設策略是不執行任何操作(讓外來鍵設定為父項識別碼),但 has_many
:through
除外,其預設策略為 delete_all
(刪除連接記錄,而不執行其回呼)。
還有一個方法 clear
,它與 delete_all
相同,只不過它會傳回關聯,而不是已刪除的記錄。
會刪除什麼?
這裡有一個潛在的陷阱:has_and_belongs_to_many
和 has_many
:through
關聯在聯結資料表中具有記錄,以及關聯記錄。因此,當我們呼叫其中一個刪除方法時,究竟應該刪除什麼?
答案是,假設關聯上的刪除是指移除擁有者與關聯物件之間的連結,而不是一定移除關聯物件本身。因此,對於 has_and_belongs_to_many
和 has_many
:through
,聯結記錄將會被刪除,但關聯記錄不會。
如果你仔細想想,這是有道理的:如果你要呼叫 post.tags.delete(Tag.find_by(name: 'food'))
,你會希望「food」標籤與貼文取消連結,而不是從資料庫中移除標籤本身。
但是,有些範例這種策略沒有意義。例如,假設一個人有很多專案,而每個專案都有很多任務。如果我們刪除一個人的任務,我們可能不希望專案被刪除。在這種情況下,刪除方法實際上無法運作:它只能在聯結模型上的關聯是 belongs_to
時使用。在其他情況下,預期你會直接對關聯記錄或 :through
關聯執行作業。
對於一般的 has_many
,沒有「關聯記錄」和「連結」的區別,因此對於要刪除什麼只有一個選擇。
對於 has_and_belongs_to_many
和 has_many
:through
,如果你要刪除關聯記錄本身,你可以隨時執行類似 person.tasks.each(&:destroy)
的操作。
使用 ActiveRecord::AssociationTypeMismatch
進行類型安全檢查
如果你嘗試將物件指定給與推論或指定的 :class_name
不相符的關聯,你會收到 ActiveRecord::AssociationTypeMismatch
。
選項
所有關聯巨集都可以透過選項進行專門化。這使得案例比簡單且可猜測的案例更複雜。
實例公開方法
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_counter
和CounterCache::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_touch
、after_commit
和after_rollback
回呼。 :inverse_of
-
指定關聯物件上
has_one
或has_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]
來源: 顯示 | 在 GitHub 上
# 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
collection<<(object, ...)
-
透過在聯結表格中建立關聯,將一個或多個物件新增到集合中(
collection.push
和collection.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
範例
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
來源:顯示 | 在 GitHub 上
# 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
collection<<(object, ...)
-
透過將其外來鍵設定為集合的主鍵,將一個或多個物件新增到集合中。請注意,此操作會立即觸發更新 SQL,而不會等到父物件的儲存或更新呼叫,除非父物件是新記錄。這也會執行關聯物件的驗證和回呼。
collection.delete(object, ...)
-
透過將其外來鍵設定為
NULL
來移除集合中的一個或多個物件。如果物件與dependent: :destroy
關聯,將會另外將其銷毀,如果物件與dependent: :delete_all
關聯,將會將其刪除。如果使用
:through
選項,則預設會刪除 (而非將其設為空值) 連接記錄,但您可以指定dependent: :destroy
或dependent: :nullify
來覆寫此設定。 collection.destroy(object, ...)
-
透過對每個記錄執行
destroy
來移除集合中的一個或多個物件,不論任何依賴選項,確保執行回呼。如果使用
:through
選項,則會銷毀連接記錄,而非物件本身。 collection=objects
-
透過適當地刪除和新增物件來取代集合內容。如果
:through
選項為 true,則會觸發連接模型中的回呼,但不會觸發銷毀回呼,因為預設會直接刪除。您可以指定dependent: :destroy
或dependent: :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
範例
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]
來源:顯示 | 在 GitHub 上
# 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_one
或belongs_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_touch
、after_commit
和after_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]
來源:顯示 | 在 GitHub 上
# 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