# Active Record 關聯
本文介紹 Active Record 中的關聯功能。
讀完本文,你將學到:
* 如何聲明 Active Record 模型間的關聯;
* 怎么理解不同的 Active Record 關聯類型;
* 如何使用關聯添加的方法;
### Chapters
1. [為什么要使用關聯](#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E4%BD%BF%E7%94%A8%E5%85%B3%E8%81%94)
2. [關聯的類型](#%E5%85%B3%E8%81%94%E7%9A%84%E7%B1%BB%E5%9E%8B)
* [`belongs_to` 關聯](#belongs_to-%E5%85%B3%E8%81%94)
* [`has_one` 關聯](#has_one-%E5%85%B3%E8%81%94)
* [`has_many` 關聯](#has_many-%E5%85%B3%E8%81%94)
* [`has_many :through` 關聯](#has_many-:through-%E5%85%B3%E8%81%94)
* [`has_one :through` 關聯](#has_one-:through-%E5%85%B3%E8%81%94)
* [`has_and_belongs_to_many` 關聯](#has_and_belongs_to_many-%E5%85%B3%E8%81%94)
* [使用 `belongs_to` 還是 `has_one`](#%E4%BD%BF%E7%94%A8-belongs_to-%E8%BF%98%E6%98%AF-has_one)
* [使用 `has_many :through` 還是 `has_and_belongs_to_many`](#%E4%BD%BF%E7%94%A8-has_many-:through-%E8%BF%98%E6%98%AF-has_and_belongs_to_many)
* [多態關聯](#%E5%A4%9A%E6%80%81%E5%85%B3%E8%81%94)
* [自連接](#%E8%87%AA%E8%BF%9E%E6%8E%A5)
3. [小技巧和注意事項](#%E5%B0%8F%E6%8A%80%E5%B7%A7%E5%92%8C%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)
* [緩存控制](#%E7%BC%93%E5%AD%98%E6%8E%A7%E5%88%B6)
* [避免命名沖突](#%E9%81%BF%E5%85%8D%E5%91%BD%E5%90%8D%E5%86%B2%E7%AA%81)
* [更新模式](#%E6%9B%B4%E6%96%B0%E6%A8%A1%E5%BC%8F)
* [控制關聯的作用域](#%E6%8E%A7%E5%88%B6%E5%85%B3%E8%81%94%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9F)
* [雙向關聯](#%E5%8F%8C%E5%90%91%E5%85%B3%E8%81%94)
4. [關聯詳解](#%E5%85%B3%E8%81%94%E8%AF%A6%E8%A7%A3)
* [`belongs_to` 關聯詳解](#belongs_to-%E5%85%B3%E8%81%94%E8%AF%A6%E8%A7%A3)
* [`has_one` 關聯詳解](#has_one-%E5%85%B3%E8%81%94%E8%AF%A6%E8%A7%A3)
* [`has_many` 關聯詳解](#has_many-%E5%85%B3%E8%81%94%E8%AF%A6%E8%A7%A3)
* [`has_and_belongs_to_many` 關聯詳解](#has_and_belongs_to_many-%E5%85%B3%E8%81%94%E8%AF%A6%E8%A7%A3)
* [關聯回調](#%E5%85%B3%E8%81%94%E5%9B%9E%E8%B0%83)
* [關聯擴展](#%E5%85%B3%E8%81%94%E6%89%A9%E5%B1%95)
### 1 為什么要使用關聯
模型之間為什么要有關聯?因為關聯讓常規操作更簡單。例如,在一個簡單的 Rails 程序中,有一個顧客模型和一個訂單模型。每個顧客可以下多個訂單。沒用關聯的模型定義如下:
```
class Customer < ActiveRecord::Base
end
class Order < ActiveRecord::Base
end
```
假如我們要為一個顧客添加一個訂單,得這么做:
```
@order = Order.create(order_date: Time.now, customer_id: @customer.id)
```
或者說要刪除一個顧客,確保他的所有訂單都會被刪除,得這么做:
```
@orders = Order.where(customer_id: @customer.id)
@orders.each do |order|
order.destroy
end
@customer.destroy
```
使用 Active Record 關聯,告訴 Rails 這兩個模型是有一定聯系的,就可以把這些操作連在一起。下面使用關聯重新定義顧客和訂單模型:
```
class Customer < ActiveRecord::Base
has_many :orders, dependent: :destroy
end
class Order < ActiveRecord::Base
belongs_to :customer
end
```
這么修改之后,為某個顧客添加新訂單就變得簡單了:
```
@order = @customer.orders.create(order_date: Time.now)
```
刪除顧客及其所有訂單更容易:
```
@customer.destroy
```
學習更多關聯類型,請閱讀下一節。下一節介紹了一些使用關聯時的小技巧,然后列出了關聯添加的所有方法和選項。
### 2 關聯的類型
在 Rails 中,關聯是兩個 Active Record 模型之間的關系。關聯使用宏的方式實現,用聲明的形式為模型添加功能。例如,聲明一個模型屬于(`belongs_to`)另一個模型后,Rails 會維護兩個模型之間的“主鍵-外鍵”關系,而且還向模型中添加了很多實用的方法。Rails 支持六種關聯:
* `belongs_to`
* `has_one`
* `has_many`
* `has_many :through`
* `has_one :through`
* `has_and_belongs_to_many`
在后面的幾節中,你會學到如何聲明并使用這些關聯。首先來看一下各種關聯適用的場景。
#### 2.1 `belongs_to` 關聯
`belongs_to` 關聯創建兩個模型之間一對一的關系,聲明所在的模型實例屬于另一個模型的實例。例如,如果程序中有顧客和訂單兩個模型,每個訂單只能指定給一個顧客,就要這么聲明訂單模型:
```
class Order < ActiveRecord::Base
belongs_to :customer
end
```

在 `belongs_to` 關聯聲明中必須使用單數形式。如果在上面的代碼中使用復數形式,程序會報錯,提示未初始化常量 `Order::Customers`。因為 Rails 自動使用關聯中的名字引用類名。如果關聯中的名字錯誤的使用復數,引用的類也就變成了復數。
相應的遷移如下:
```
class CreateOrders < ActiveRecord::Migration
def change
create_table :customers do |t|
t.string :name
t.timestamps
end
create_table :orders do |t|
t.belongs_to :customer
t.datetime :order_date
t.timestamps
end
end
end
```
#### 2.2 `has_one` 關聯
`has_one` 關聯也會建立兩個模型之間的一對一關系,但語義和結果有點不一樣。這種關聯表示模型的實例包含或擁有另一個模型的實例。例如,在程序中,每個供應商只有一個賬戶,可以這么定義供應商模型:
```
class Supplier < ActiveRecord::Base
has_one :account
end
```

相應的遷移如下:
```
class CreateSuppliers < ActiveRecord::Migration
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier
t.string :account_number
t.timestamps
end
end
end
```
#### 2.3 `has_many` 關聯
`has_many` 關聯建立兩個模型之間的一對多關系。在 `belongs_to` 關聯的另一端經常會使用這個關聯。`has_many` 關聯表示模型的實例有零個或多個另一個模型的實例。例如,在程序中有顧客和訂單兩個模型,顧客模型可以這么定義:
```
class Customer < ActiveRecord::Base
has_many :orders
end
```
聲明 `has_many` 關聯時,另一個模型使用復數形式。

相應的遷移如下:
```
class CreateCustomers < ActiveRecord::Migration
def change
create_table :customers do |t|
t.string :name
t.timestamps
end
create_table :orders do |t|
t.belongs_to :customer
t.datetime :order_date
t.timestamps
end
end
end
```
#### 2.4 `has_many :through` 關聯
`has_many :through` 關聯經常用來建立兩個模型之間的多對多關聯。這種關聯表示一個模型的實例可以借由第三個模型,擁有零個和多個另一個模型的實例。例如,在看病過程中,病人要和醫生預約時間。這中間的關聯聲明如下:
```
class Physician < ActiveRecord::Base
has_many :appointments
has_many :patients, through: :appointments
end
class Appointment < ActiveRecord::Base
belongs_to :physician
belongs_to :patient
end
class Patient < ActiveRecord::Base
has_many :appointments
has_many :physicians, through: :appointments
end
```

相應的遷移如下:
```
class CreateAppointments < ActiveRecord::Migration
def change
create_table :physicians do |t|
t.string :name
t.timestamps
end
create_table :patients do |t|
t.string :name
t.timestamps
end
create_table :appointments do |t|
t.belongs_to :physician
t.belongs_to :patient
t.datetime :appointment_date
t.timestamps
end
end
end
```
連接模型中的集合可以使用 API 關聯。例如:
```
physician.patients = patients
```
會為新建立的關聯對象創建連接模型實例,如果其中一個對象刪除了,相應的記錄也會刪除。
自動刪除連接模型的操作直接執行,不會觸發 `*_destroy` 回調。
`has_many :through` 還可用來簡化嵌套的 `has_many` 關聯。例如,一個文檔分為多個部分,每一部分又有多個段落,如果想使用簡單的方式獲取文檔中的所有段落,可以這么做:
```
class Document < ActiveRecord::Base
has_many :sections
has_many :paragraphs, through: :sections
end
class Section < ActiveRecord::Base
belongs_to :document
has_many :paragraphs
end
class Paragraph < ActiveRecord::Base
belongs_to :section
end
```
加上 `through: :sections` 后,Rails 就能理解這段代碼:
```
@document.paragraphs
```
#### 2.5 `has_one :through` 關聯
`has_one :through` 關聯建立兩個模型之間的一對一關系。這種關聯表示一個模型通過第三個模型擁有另一個模型的實例。例如,每個供應商只有一個賬戶,而且每個賬戶都有一個歷史賬戶,那么可以這么定義模型:
```
class Supplier < ActiveRecord::Base
has_one :account
has_one :account_history, through: :account
end
class Account < ActiveRecord::Base
belongs_to :supplier
has_one :account_history
end
class AccountHistory < ActiveRecord::Base
belongs_to :account
end
```

相應的遷移如下:
```
class CreateAccountHistories < ActiveRecord::Migration
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier
t.string :account_number
t.timestamps
end
create_table :account_histories do |t|
t.belongs_to :account
t.integer :credit_rating
t.timestamps
end
end
end
```
#### 2.6 `has_and_belongs_to_many` 關聯
`has_and_belongs_to_many` 關聯之間建立兩個模型之間的多對多關系,不借由第三個模型。例如,程序中有裝配體和零件兩個模型,每個裝配體中有多個零件,每個零件又可用于多個裝配體,這時可以按照下面的方式定義模型:
```
class Assembly < ActiveRecord::Base
has_and_belongs_to_many :parts
end
class Part < ActiveRecord::Base
has_and_belongs_to_many :assemblies
end
```

相應的遷移如下:
```
class CreateAssembliesAndParts < ActiveRecord::Migration
def change
create_table :assemblies do |t|
t.string :name
t.timestamps
end
create_table :parts do |t|
t.string :part_number
t.timestamps
end
create_table :assemblies_parts, id: false do |t|
t.belongs_to :assembly
t.belongs_to :part
end
end
end
```
#### 2.7 使用 `belongs_to` 還是 `has_one`
如果想建立兩個模型之間的一對一關系,可以在一個模型中聲明 `belongs_to`,然后在另一模型中聲明 `has_one`。但是怎么知道在哪個模型中聲明哪種關聯?
不同的聲明方式帶來的區別是外鍵放在哪個模型對應的數據表中(外鍵在聲明 `belongs_to` 關聯所在模型對應的數據表中)。不過聲明時要考慮一下語義,`has_one` 的意思是某樣東西屬于我。例如,說供應商有一個賬戶,比賬戶擁有供應商更合理,所以正確的關聯應該這么聲明:
```
class Supplier < ActiveRecord::Base
has_one :account
end
class Account < ActiveRecord::Base
belongs_to :supplier
end
```
相應的遷移如下:
```
class CreateSuppliers < ActiveRecord::Migration
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.integer :supplier_id
t.string :account_number
t.timestamps
end
end
end
```
`t.integer :supplier_id` 更明確的表明了外鍵的名字。在目前的 Rails 版本中,可以抽象實現的細節,使用 `t.references :supplier` 代替。
#### 2.8 使用 `has_many :through` 還是 `has_and_belongs_to_many`
Rails 提供了兩種建立模型之間多對多關系的方法。其中比較簡單的是 `has_and_belongs_to_many`,可以直接建立關聯:
```
class Assembly < ActiveRecord::Base
has_and_belongs_to_many :parts
end
class Part < ActiveRecord::Base
has_and_belongs_to_many :assemblies
end
```
第二種方法是使用 `has_many :through`,但無法直接建立關聯,要通過第三個模型:
```
class Assembly < ActiveRecord::Base
has_many :manifests
has_many :parts, through: :manifests
end
class Manifest < ActiveRecord::Base
belongs_to :assembly
belongs_to :part
end
class Part < ActiveRecord::Base
has_many :manifests
has_many :assemblies, through: :manifests
end
```
根據經驗,如果關聯的第三個模型要作為獨立實體使用,要用 `has_many :through` 關聯;如果不需要使用第三個模型,用簡單的 `has_and_belongs_to_many` 關聯即可(不過要記得在數據庫中創建連接數據表)。
如果需要做數據驗證、回調,或者連接模型上要用到其他屬性,此時就要使用 `has_many :through` 關聯。
#### 2.9 多態關聯
關聯還有一種高級用法,“多態關聯”。在多態關聯中,在同一個關聯中,模型可以屬于其他多個模型。例如,圖片模型可以屬于雇員模型或者產品模型,模型的定義如下:
```
class Picture < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
end
class Employee < ActiveRecord::Base
has_many :pictures, as: :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, as: :imageable
end
```
在 `belongs_to` 中指定使用多態,可以理解成創建了一個接口,可供任何一個模型使用。在 `Employee` 模型實例上,可以使用 `@employee.pictures` 獲取圖片集合。類似地,可使用 `@product.pictures` 獲取產品的圖片。
在 `Picture` 模型的實例上,可以使用 `@picture.imageable` 獲取父對象。不過事先要在聲明多態接口的模型中創建外鍵字段和類型字段:
```
class CreatePictures < ActiveRecord::Migration
def change
create_table :pictures do |t|
t.string :name
t.integer :imageable_id
t.string :imageable_type
t.timestamps
end
end
end
```
上面的遷移可以使用 `t.references` 簡化:
```
class CreatePictures < ActiveRecord::Migration
def change
create_table :pictures do |t|
t.string :name
t.references :imageable, polymorphic: true
t.timestamps
end
end
end
```

#### 2.10 自連接
設計數據模型時會發現,有時模型要和自己建立關聯。例如,在一個數據表中保存所有雇員的信息,但要建立經理和下屬之間的關系。這種情況可以使用自連接關聯解決:
```
class Employee < ActiveRecord::Base
has_many :subordinates, class_name: "Employee",
foreign_key: "manager_id"
belongs_to :manager, class_name: "Employee"
end
```
這樣定義模型后,就可以使用 `@employee.subordinates` 和 `@employee.manager` 了。
在遷移中,要添加一個引用字段,指向模型自身:
```
class CreateEmployees < ActiveRecord::Migration
def change
create_table :employees do |t|
t.references :manager
t.timestamps
end
end
end
```
### 3 小技巧和注意事項
在 Rails 程序中高效地使用 Active Record 關聯,要了解以下幾個知識:
* 緩存控制
* 避免命名沖突
* 更新模式
* 控制關聯的作用域
* Bi-directional associations
#### 3.1 緩存控制
關聯添加的方法都會使用緩存,記錄最近一次查詢結果,以備后用。緩存還會在方法之間共享。例如:
```
customer.orders # retrieves orders from the database
customer.orders.size # uses the cached copy of orders
customer.orders.empty? # uses the cached copy of orders
```
程序的其他部分會修改數據,那么應該怎么重載緩存呢?調用關聯方法時傳入 `true` 參數即可:
```
customer.orders # retrieves orders from the database
customer.orders.size # uses the cached copy of orders
customer.orders(true).empty? # discards the cached copy of orders
# and goes back to the database
```
#### 3.2 避免命名沖突
關聯的名字并不能隨意使用。因為創建關聯時,會向模型添加同名方法,所以關聯的名字不能和 `ActiveRecord::Base` 中的實例方法同名。如果同名,關聯方法會覆蓋 `ActiveRecord::Base` 中的實例方法,導致錯誤。例如,關聯的名字不能為 `attributes` 或 `connection`。
#### 3.3 更新模式
關聯非常有用,但沒什么魔法。關聯對應的數據庫模式需要你自己編寫。不同的關聯類型,要做的事也不同。對 `belongs_to` 關聯來說,要創建外鍵;對 `has_and_belongs_to_many` 來說,要創建相應的連接數據表。
##### 3.3.1 創建 `belongs_to` 關聯所需的外鍵
聲明 `belongs_to` 關聯后,要創建相應的外鍵。例如,有下面這個模型:
```
class Order < ActiveRecord::Base
belongs_to :customer
end
```
這種關聯需要在數據表中創建合適的外鍵:
```
class CreateOrders < ActiveRecord::Migration
def change
create_table :orders do |t|
t.datetime :order_date
t.string :order_number
t.integer :customer_id
end
end
end
```
如果聲明關聯之前已經定義了模型,則要在遷移中使用 `add_column` 創建外鍵。
##### 3.3.2 創建 `has_and_belongs_to_many` 關聯所需的連接數據表
聲明 `has_and_belongs_to_many` 關聯后,必須手動創建連接數據表。除非在 `:join_table` 選項中指定了連接數據表的名字,否則 Active Record 會按照類名出現在字典中的順序為數據表起名字。那么,顧客和訂單模型使用的連接數據表默認名為“customers_orders”,因為在字典中,“c”在“o”前面。
模型名的順序使用字符串的 `<` 操作符確定。所以,如果兩個字符串的長度不同,比較最短長度時,兩個字符串是相等的,但長字符串的排序比短字符串靠前。例如,你可能以為“"paper_boxes”和“papers”這兩個表生成的連接表名為“papers_paper_boxes”,因為“paper_boxes”比“papers”長。其實生成的連接表名為“paper_boxes_papers”,因為在一般的編碼方式中,“_”比“s”靠前。
不管名字是什么,你都要在遷移中手動創建連接數據表。例如下面的關聯聲明:
```
class Assembly < ActiveRecord::Base
has_and_belongs_to_many :parts
end
class Part < ActiveRecord::Base
has_and_belongs_to_many :assemblies
end
```
需要在遷移中創建 `assemblies_parts` 數據表,而且該表無主鍵:
```
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration
def change
create_table :assemblies_parts, id: false do |t|
t.integer :assembly_id
t.integer :part_id
end
end
end
```
我們把 `id: false` 選項傳給 `create_table` 方法,因為這個表不對應模型。只有這樣,關聯才能正常建立。如果在使用 `has_and_belongs_to_many` 關聯時遇到奇怪的表現,例如提示模型 ID 損壞,或 ID 沖突,有可能就是因為創建了主鍵。
#### 3.4 控制關聯的作用域
默認情況下,關聯只會查找當前模塊作用域中的對象。如果在模塊中定義 Active Record 模型,知道這一點很重要。例如:
```
module MyApplication
module Business
class Supplier < ActiveRecord::Base
has_one :account
end
class Account < ActiveRecord::Base
belongs_to :supplier
end
end
end
```
上面的代碼能正常運行,因為 `Supplier` 和 `Account` 在同一個作用域內。但下面這段代碼就不行了,因為 `Supplier` 和 `Account` 在不同的作用域中:
```
module MyApplication
module Business
class Supplier < ActiveRecord::Base
has_one :account
end
end
module Billing
class Account < ActiveRecord::Base
belongs_to :supplier
end
end
end
```
要想讓處在不同命名空間中的模型正常建立關聯,聲明關聯時要指定完整的類名:
```
module MyApplication
module Business
class Supplier < ActiveRecord::Base
has_one :account,
class_name: "MyApplication::Billing::Account"
end
end
module Billing
class Account < ActiveRecord::Base
belongs_to :supplier,
class_name: "MyApplication::Business::Supplier"
end
end
end
```
#### 3.5 雙向關聯
一般情況下,都要求能在關聯的兩端進行操作。例如,有下面的關聯聲明:
```
class Customer < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :customer
end
```
默認情況下,Active Record 并不知道這個關聯中兩個模型之間的聯系。可能導致同一對象的兩個副本不同步:
```
c = Customer.first
o = c.orders.first
c.first_name == o.customer.first_name # => true
c.first_name = 'Manny'
c.first_name == o.customer.first_name # => false
```
之所以會發生這種情況,是因為 `c` 和 `o.customer` 在內存中是同一數據的兩種表示,修改其中一個并不會刷新另一個。Active Record 提供了 `:inverse_of` 選項,可以告知 Rails 兩者之間的關系:
```
class Customer < ActiveRecord::Base
has_many :orders, inverse_of: :customer
end
class Order < ActiveRecord::Base
belongs_to :customer, inverse_of: :orders
end
```
這么修改之后,Active Record 就只會加載一個顧客對象,避免數據的不一致性,提高程序的執行效率:
```
c = Customer.first
o = c.orders.first
c.first_name == o.customer.first_name # => true
c.first_name = 'Manny'
c.first_name == o.customer.first_name # => true
```
`inverse_of` 有些限制:
* 不能和 `:through` 選項同時使用;
* 不能和 `:polymorphic` 選項同時使用;
* 不能和 `:as` 選項同時使用;
* 在 `belongs_to` 關聯中,會忽略 `has_many` 關聯的 `inverse_of` 選項;
每種關聯都會嘗試自動找到關聯的另一端,設置 `:inverse_of` 選項(根據關聯的名字)。使用標準名字的關聯都有這種功能。但是,如果在關聯中設置了下面這些選項,將無法自動設置 `:inverse_of`:
* `:conditions`
* `:through`
* `:polymorphic`
* `:foreign_key`
### 4 關聯詳解
下面幾節詳細說明各種關聯,包括添加的方法和聲明關聯時可以使用的選項。
#### 4.1 `belongs_to` 關聯詳解
`belongs_to` 關聯創建一個模型與另一個模型之間的一對一關系。用數據庫的行話來說,就是這個類中包含了外鍵。如果外鍵在另一個類中,就應該使用 `has_one` 關聯。
##### 4.1.1 `belongs_to` 關聯添加的方法
聲明 `belongs_to` 關聯后,所在的類自動獲得了五個和關聯相關的方法:
* `association(force_reload = false)`
* `association=(associate)`
* `build_association(attributes = {})`
* `create_association(attributes = {})`
* `create_association!(attributes = {})`
這五個方法中的 `association` 要替換成傳入 `belongs_to` 方法的第一個參數。例如,如下的聲明:
```
class Order < ActiveRecord::Base
belongs_to :customer
end
```
每個 `Order` 模型實例都獲得了這些方法:
```
customer
customer=
build_customer
create_customer
create_customer!
```
在 `has_one` 和 `belongs_to` 關聯中,必須使用 `build_*` 方法構建關聯對象。`association.build` 方法是在 `has_many` 和 `has_and_belongs_to_many` 關聯中使用的。創建關聯對象要使用 `create_*` 方法。
###### 4.1.1.1 `association(force_reload = false)`
如果關聯的對象存在,`association` 方法會返回關聯對象。如果找不到關聯對象,則返回 `nil`。
```
@customer = @order.customer
```
如果關聯對象之前已經取回,會返回緩存版本。如果不想使用緩存版本,強制重新從數據庫中讀取,可以把 `force_reload` 參數設為 `true`。
###### 4.1.1.2 `association=(associate)`
`association=` 方法用來賦值關聯的對象。這個方法的底層操作是,從關聯對象上讀取主鍵,然后把值賦給該主鍵對應的對象。
```
@order.customer = @customer
```
###### 4.1.1.3 `build_association(attributes = {})`
`build_association` 方法返回該關聯類型的一個新對象。這個對象使用傳入的屬性初始化,和對象連接的外鍵會自動設置,但關聯對象不會存入數據庫。
```
@customer = @order.build_customer(customer_number: 123,
customer_name: "John Doe")
```
###### 4.1.1.4 `create_association(attributes = {})`
`create_association` 方法返回該關聯類型的一個新對象。這個對象使用傳入的屬性初始化,和對象連接的外鍵會自動設置,只要能通過所有數據驗證,就會把關聯對象存入數據庫。
```
@customer = @order.create_customer(customer_number: 123,
customer_name: "John Doe")
```
###### 4.1.1.5 `create_association!(attributes = {})`
和 `create_association` 方法作用相同,但是如果記錄不合法,會拋出 `ActiveRecord::RecordInvalid` 異常。
##### 4.1.2 `belongs_to` 方法的選項
Rails 的默認設置足夠智能,能滿足常見需求。但有時還是需要定制 `belongs_to` 關聯的行為。定制的方法很簡單,聲明關聯時傳入選項或者使用代碼塊即可。例如,下面的關聯使用了兩個選項:
```
class Order < ActiveRecord::Base
belongs_to :customer, dependent: :destroy,
counter_cache: true
end
```
`belongs_to` 關聯支持以下選項:
* `:autosave`
* `:class_name`
* `:counter_cache`
* `:dependent`
* `:foreign_key`
* `:inverse_of`
* `:polymorphic`
* `:touch`
* `:validate`
###### 4.1.2.1 `:autosave`
如果把 `:autosave` 選項設為 `true`,保存父對象時,會自動保存所有子對象,并把標記為析構的子對象銷毀。
###### 4.1.2.2 `:class_name`
如果另一個模型無法從關聯的名字獲取,可以使用 `:class_name` 選項指定模型名。例如,如果訂單屬于顧客,但表示顧客的模型是 `Patron`,就可以這樣聲明關聯:
```
class Order < ActiveRecord::Base
belongs_to :customer, class_name: "Patron"
end
```
###### 4.1.2.3 `:counter_cache`
`:counter_cache` 選項可以提高統計所屬對象數量操作的效率。假如如下的模型:
```
class Order < ActiveRecord::Base
belongs_to :customer
end
class Customer < ActiveRecord::Base
has_many :orders
end
```
這樣聲明關聯后,如果想知道 `@customer.orders.size` 的結果,就要在數據庫中執行 `COUNT(*)` 查詢。如果不想執行這個查詢,可以在聲明 `belongs_to` 關聯的模型中加入計數緩存功能:
```
class Order < ActiveRecord::Base
belongs_to :customer, counter_cache: true
end
class Customer < ActiveRecord::Base
has_many :orders
end
```
這樣聲明關聯后,Rails 會及時更新緩存,調用 `size` 方法時返回緩存中的值。
雖然 `:counter_cache` 選項在聲明 `belongs_to` 關聯的模型中設置,但實際使用的字段要添加到關聯的模型中。針對上面的例子,要把 `orders_count` 字段加入 `Customer` 模型。這個字段的默認名也是可以設置的:
```
class Order < ActiveRecord::Base
belongs_to :customer, counter_cache: :count_of_orders
end
class Customer < ActiveRecord::Base
has_many :orders
end
```
計數緩存字段通過 `attr_readonly` 方法加入關聯模型的只讀屬性列表中。
###### 4.1.2.4 `:dependent`
`:dependent` 選項的值有兩個:
* `:destroy`:銷毀對象時,也會在關聯對象上調用 `destroy` 方法;
* `:delete`:銷毀對象時,關聯的對象不會調用 `destroy` 方法,而是直接從數據庫中刪除;
在 `belongs_to` 關聯和 `has_many` 關聯配對時,不應該設置這個選項,否則會導致數據庫中出現孤兒記錄。
###### 4.1.2.5 `:foreign_key`
按照約定,用來存儲外鍵的字段名是關聯名后加 `_id`。`:foreign_key` 選項可以設置要使用的外鍵名:
```
class Order < ActiveRecord::Base
belongs_to :customer, class_name: "Patron",
foreign_key: "patron_id"
end
```
不管怎樣,Rails 都不會自動創建外鍵字段,你要自己在遷移中創建。
###### 4.1.2.6 `:inverse_of`
`:inverse_of` 選項指定 `belongs_to` 關聯另一端的 `has_many` 和 `has_one` 關聯名。不能和 `:polymorphic` 選項一起使用。
```
class Customer < ActiveRecord::Base
has_many :orders, inverse_of: :customer
end
class Order < ActiveRecord::Base
belongs_to :customer, inverse_of: :orders
end
```
###### 4.1.2.7 `:polymorphic`
`:polymorphic` 選項為 `true` 時表明這是個多態關聯。[前文](#polymorphic-associations)已經詳細介紹過多態關聯。
###### 4.1.2.8 `:touch`
如果把 `:touch` 選項設為 `true`,保存或銷毀對象時,關聯對象的 `updated_at` 或 `updated_on` 字段會自動設為當前時間戳。
```
class Order < ActiveRecord::Base
belongs_to :customer, touch: true
end
class Customer < ActiveRecord::Base
has_many :orders
end
```
在這個例子中,保存或銷毀訂單后,會更新關聯的顧客中的時間戳。還可指定要更新哪個字段的時間戳:
```
class Order < ActiveRecord::Base
belongs_to :customer, touch: :orders_updated_at
end
```
###### 4.1.2.9 `:validate`
如果把 `:validate` 選項設為 `true`,保存對象時,會同時驗證關聯對象。該選項的默認值是 `false`,保存對象時不驗證關聯對象。
##### 4.1.3 `belongs_to` 的作用域
有時可能需要定制 `belongs_to` 關聯使用的查詢方式,定制的查詢可在作用域代碼塊中指定。例如:
```
class Order < ActiveRecord::Base
belongs_to :customer, -> { where active: true },
dependent: :destroy
end
```
在作用域代碼塊中可以使用任何一個標準的[查詢方法](active_record_querying.html)。下面分別介紹這幾個方法:
* `where`
* `includes`
* `readonly`
* `select`
###### 4.1.3.1 `where`
`where` 方法指定關聯對象必須滿足的條件。
```
class Order < ActiveRecord::Base
belongs_to :customer, -> { where active: true }
end
```
###### 4.1.3.2 `includes`
`includes` 方法指定使用關聯時要按需加載的間接關聯。例如,有如下的模型:
```
class LineItem < ActiveRecord::Base
belongs_to :order
end
class Order < ActiveRecord::Base
belongs_to :customer
has_many :line_items
end
class Customer < ActiveRecord::Base
has_many :orders
end
```
如果經常要直接從商品上獲取顧客對象(`@line_item.order.customer`),就可以把顧客引入商品和訂單的關聯中:
```
class LineItem < ActiveRecord::Base
belongs_to :order, -> { includes :customer }
end
class Order < ActiveRecord::Base
belongs_to :customer
has_many :line_items
end
class Customer < ActiveRecord::Base
has_many :orders
end
```
直接關聯沒必要使用 `includes`。如果 `Order belongs_to :customer`,那么顧客會自動按需加載。
###### 4.1.3.3 `readonly`
如果使用 `readonly`,通過關聯獲取的對象就是只讀的。
###### 4.1.3.4 `select`
`select` 方法會覆蓋獲取關聯對象使用的 SQL `SELECT` 子句。默認情況下,Rails 會讀取所有字段。
如果在 `belongs_to` 關聯中使用 `select` 方法,應該同時設置 `:foreign_key` 選項,確保返回正確的結果。
##### 4.1.4 檢查關聯的對象是否存在
檢查關聯的對象是否存在可以使用 `association.nil?` 方法:
```
if @order.customer.nil?
@msg = "No customer found for this order"
end
```
##### 4.1.5 什么時候保存對象
把對象賦值給 `belongs_to` 關聯不會自動保存對象,也不會保存關聯的對象。
#### 4.2 `has_one` 關聯詳解
`has_one` 關聯建立兩個模型之間的一對一關系。用數據庫的行話說,這種關聯的意思是外鍵在另一個類中。如果外鍵在這個類中,應該使用 `belongs_to` 關聯。
##### 4.2.1 `has_one` 關聯添加的方法
聲明 `has_one` 關聯后,聲明所在的類自動獲得了五個關聯相關的方法:
* `association(force_reload = false)`
* `association=(associate)`
* `build_association(attributes = {})`
* `create_association(attributes = {})`
* `create_association!(attributes = {})`
這五個方法中的 `association` 要替換成傳入 `has_one` 方法的第一個參數。例如,如下的聲明:
```
class Supplier < ActiveRecord::Base
has_one :account
end
```
每個 `Supplier` 模型實例都獲得了這些方法:
```
account
account=
build_account
create_account
create_account!
```
在 `has_one` 和 `belongs_to` 關聯中,必須使用 `build_*` 方法構建關聯對象。`association.build` 方法是在 `has_many` 和 `has_and_belongs_to_many` 關聯中使用的。創建關聯對象要使用 `create_*` 方法。
###### 4.2.1.1 `association(force_reload = false)`
如果關聯的對象存在,`association` 方法會返回關聯對象。如果找不到關聯對象,則返回 `nil`。
```
@account = @supplier.account
```
如果關聯對象之前已經取回,會返回緩存版本。如果不想使用緩存版本,強制重新從數據庫中讀取,可以把 `force_reload` 參數設為 `true`。
###### 4.2.1.2 `association=(associate)`
`association=` 方法用來賦值關聯的對象。這個方法的底層操作是,從關聯對象上讀取主鍵,然后把值賦給該主鍵對應的關聯對象。
```
@supplier.account = @account
```
###### 4.2.1.3 `build_association(attributes = {})`
`build_association` 方法返回該關聯類型的一個新對象。這個對象使用傳入的屬性初始化,和對象連接的外鍵會自動設置,但關聯對象不會存入數據庫。
```
@account = @supplier.build_account(terms: "Net 30")
```
###### 4.2.1.4 `create_association(attributes = {})`
`create_association` 方法返回該關聯類型的一個新對象。這個對象使用傳入的屬性初始化,和對象連接的外鍵會自動設置,只要能通過所有數據驗證,就會把關聯對象存入數據庫。
```
@account = @supplier.create_account(terms: "Net 30")
```
###### 4.2.1.5 `create_association!(attributes = {})`
和 `create_association` 方法作用相同,但是如果記錄不合法,會拋出 `ActiveRecord::RecordInvalid` 異常。
##### 4.2.2 `has_one` 方法的選項
Rails 的默認設置足夠智能,能滿足常見需求。但有時還是需要定制 `has_one` 關聯的行為。定制的方法很簡單,聲明關聯時傳入選項即可。例如,下面的關聯使用了兩個選項:
```
class Supplier < ActiveRecord::Base
has_one :account, class_name: "Billing", dependent: :nullify
end
```
`has_one` 關聯支持以下選項:
* `:as`
* `:autosave`
* `:class_name`
* `:dependent`
* `:foreign_key`
* `:inverse_of`
* `:primary_key`
* `:source`
* `:source_type`
* `:through`
* `:validate`
###### 4.2.2.1 `:as`
`:as` 選項表明這是多態關聯。[前文](#polymorphic-associations)已經詳細介紹過多態關聯。
###### 4.2.2.2 `:autosave`
如果把 `:autosave` 選項設為 `true`,保存父對象時,會自動保存所有子對象,并把標記為析構的子對象銷毀。
###### 4.2.2.3 `:class_name`
如果另一個模型無法從關聯的名字獲取,可以使用 `:class_name` 選項指定模型名。例如,供應商有一個賬戶,但表示賬戶的模型是 `Billing`,就可以這樣聲明關聯:
```
class Supplier < ActiveRecord::Base
has_one :account, class_name: "Billing"
end
```
###### 4.2.2.4 `:dependent`
設置銷毀擁有者時要怎么處理關聯對象:
* `:destroy`:也銷毀關聯對象;
* `:delete`:直接把關聯對象對數據庫中刪除,因此不會執行回調;
* `:nullify`:把外鍵設為 `NULL`,不會執行回調;
* `:restrict_with_exception`:有關聯的對象時拋出異常;
* `:restrict_with_error`:有關聯的對象時,向擁有者添加一個錯誤;
如果在數據庫層設置了 `NOT NULL` 約束,就不能使用 `:nullify` 選項。如果 `:dependent` 選項沒有銷毀關聯,就無法修改關聯對象,因為關聯對象的外鍵設置為不接受 `NULL`。
###### 4.2.2.5 `:foreign_key`
按照約定,在另一個模型中用來存儲外鍵的字段名是模型名后加 `_id`。`:foreign_key` 選項可以設置要使用的外鍵名:
```
class Supplier < ActiveRecord::Base
has_one :account, foreign_key: "supp_id"
end
```
不管怎樣,Rails 都不會自動創建外鍵字段,你要自己在遷移中創建。
###### 4.2.2.6 `:inverse_of`
`:inverse_of` 選項指定 `has_one` 關聯另一端的 `belongs_to` 關聯名。不能和 `:through` 或 `:as` 選項一起使用。
```
class Supplier < ActiveRecord::Base
has_one :account, inverse_of: :supplier
end
class Account < ActiveRecord::Base
belongs_to :supplier, inverse_of: :account
end
```
###### 4.2.2.7 `:primary_key`
按照約定,用來存儲該模型主鍵的字段名 `id`。`:primary_key` 選項可以設置要使用的主鍵名。
###### 4.2.2.8 `:source`
`:source` 選項指定 `has_one :through` 關聯的關聯源名字。
###### 4.2.2.9 `:source_type`
`:source_type` 選項指定 `has_one :through` 關聯中用來處理多態關聯的關聯源類型。
###### 4.2.2.10 `:through`
`:through` 選項指定用來執行查詢的連接模型。[前文](#the-has-one-through-association)詳細介紹過 `has_one :through` 關聯。
###### 4.2.2.11 `:validate`
如果把 `:validate` 選項設為 `true`,保存對象時,會同時驗證關聯對象。該選項的默認值是 `false`,保存對象時不驗證關聯對象。
##### 4.2.3 `has_one` 的作用域
有時可能需要定制 `has_one` 關聯使用的查詢方式,定制的查詢可在作用域代碼塊中指定。例如:
```
class Supplier < ActiveRecord::Base
has_one :account, -> { where active: true }
end
```
在作用域代碼塊中可以使用任何一個標準的[查詢方法](active_record_querying.html)。下面分別介紹這幾個方法:
* `where`
* `includes`
* `readonly`
* `select`
###### 4.2.3.1 `where`
`where` 方法指定關聯對象必須滿足的條件。
```
class Supplier < ActiveRecord::Base
has_one :account, -> { where "confirmed = 1" }
end
```
###### 4.2.3.2 `includes`
`includes` 方法指定使用關聯時要按需加載的間接關聯。例如,有如下的模型:
```
class Supplier < ActiveRecord::Base
has_one :account
end
class Account < ActiveRecord::Base
belongs_to :supplier
belongs_to :representative
end
class Representative < ActiveRecord::Base
has_many :accounts
end
```
如果經常要直接獲取供應商代表(`@supplier.account.representative`),就可以把代表引入供應商和賬戶的關聯中:
```
class Supplier < ActiveRecord::Base
has_one :account, -> { includes :representative }
end
class Account < ActiveRecord::Base
belongs_to :supplier
belongs_to :representative
end
class Representative < ActiveRecord::Base
has_many :accounts
end
```
###### 4.2.3.3 `readonly`
如果使用 `readonly`,通過關聯獲取的對象就是只讀的。
###### 4.2.3.4 `select`
`select` 方法會覆蓋獲取關聯對象使用的 SQL `SELECT` 子句。默認情況下,Rails 會讀取所有字段。
##### 4.2.4 檢查關聯的對象是否存在
檢查關聯的對象是否存在可以使用 `association.nil?` 方法:
```
if @supplier.account.nil?
@msg = "No account found for this supplier"
end
```
##### 4.2.5 什么時候保存對象
把對象賦值給 `has_one` 關聯時,會自動保存對象(因為要更新外鍵)。而且所有被替換的對象也會自動保存,因為外鍵也變了。
如果無法通過驗證,隨便哪一次保存失敗了,賦值語句就會返回 `false`,賦值操作會取消。
如果父對象(`has_one` 關聯聲明所在的模型)沒保存(`new_record?` 方法返回 `true`),那么子對象也不會保存。只有保存了父對象,才會保存子對象。
如果賦值給 `has_one` 關聯時不想保存對象,可以使用 `association.build` 方法。
#### 4.3 `has_many` 關聯詳解
`has_many` 關聯建立兩個模型之間的一對多關系。用數據庫的行話說,這種關聯的意思是外鍵在另一個類中,指向這個類的實例。
##### 4.3.1 `has_many` 關聯添加的方法
聲明 `has_many` 關聯后,聲明所在的類自動獲得了 16 個關聯相關的方法:
* `collection(force_reload = false)`
* `collection<<(object, ...)`
* `collection.delete(object, ...)`
* `collection.destroy(object, ...)`
* `collection=objects`
* `collection_singular_ids`
* `collection_singular_ids=ids`
* `collection.clear`
* `collection.empty?`
* `collection.size`
* `collection.find(...)`
* `collection.where(...)`
* `collection.exists?(...)`
* `collection.build(attributes = {}, ...)`
* `collection.create(attributes = {})`
* `collection.create!(attributes = {})`
這些個方法中的 `collection` 要替換成傳入 `has_many` 方法的第一個參數。`collection_singular` 要替換成第一個參數的單數形式。例如,如下的聲明:
```
class Customer < ActiveRecord::Base
has_many :orders
end
```
每個 `Customer` 模型實例都獲得了這些方法:
```
orders(force_reload = false)
orders<<(object, ...)
orders.delete(object, ...)
orders.destroy(object, ...)
orders=objects
order_ids
order_ids=ids
orders.clear
orders.empty?
orders.size
orders.find(...)
orders.where(...)
orders.exists?(...)
orders.build(attributes = {}, ...)
orders.create(attributes = {})
orders.create!(attributes = {})
```
###### 4.3.1.1 `collection(force_reload = false)`
`collection` 方法返回一個數組,包含所有關聯的對象。如果沒有關聯的對象,則返回空數組。
```
@orders = @customer.orders
```
###### 4.3.1.2 `collection<<(object, ...)`
`collection<<` 方法向關聯對象數組中添加一個或多個對象,并把各所加對象的外鍵設為調用此方法的模型的主鍵。
```
@customer.orders << @order1
```
###### 4.3.1.3 `collection.delete(object, ...)`
`collection.delete` 方法從關聯對象數組中刪除一個或多個對象,并把刪除的對象外鍵設為 `NULL`。
```
@customer.orders.delete(@order1)
```
如果關聯設置了 `dependent: :destroy`,還會銷毀關聯對象;如果關聯設置了 `dependent: :delete_all`,還會刪除關聯對象。
###### 4.3.1.4 `collection.destroy(object, ...)`
`collection.destroy` 方法在關聯對象上調用 `destroy` 方法,從關聯對象數組中刪除一個或多個對象。
```
@customer.orders.destroy(@order1)
```
對象會從數據庫中刪除,忽略 `:dependent` 選項。
###### 4.3.1.5 `collection=objects`
`collection=` 讓關聯對象數組只包含指定的對象,根據需求會添加或刪除對象。
###### 4.3.1.6 `collection_singular_ids`
`collection_singular_ids` 返回一個數組,包含關聯對象數組中各對象的 ID。
```
@order_ids = @customer.order_ids
```
###### 4.3.1.7 `collection_singular_ids=ids`
`collection_singular_ids=` 方法讓數組中只包含指定的主鍵,根據需要增刪 ID。
###### 4.3.1.8 `collection.clear`
`collection.clear` 方法刪除數組中的所有對象。如果關聯中指定了 `dependent: :destroy` 選項,會銷毀關聯對象;如果關聯中指定了 `dependent: :delete_all` 選項,會直接從數據庫中刪除對象,然后再把外鍵設為 `NULL`。
###### 4.3.1.9 `collection.empty?`
如果關聯數組中沒有關聯對象,`collection.empty?` 方法返回 `true`。
```
<% if @customer.orders.empty? %>
No Orders Found
<% end %>
```
###### 4.3.1.10 `collection.size`
`collection.size` 返回關聯對象數組中的對象數量。
```
@order_count = @customer.orders.size
```
###### 4.3.1.11 `collection.find(...)`
`collection.find` 方法在關聯對象數組中查找對象,句法和可用選項跟 `ActiveRecord::Base.find` 方法一樣。
```
@open_orders = @customer.orders.find(1)
```
###### 4.3.1.12 `collection.where(...)`
`collection.where` 方法根據指定的條件在關聯對象數組中查找對象,但會惰性加載對象,用到對象時才會執行查詢。
```
@open_orders = @customer.orders.where(open: true) # No query yet
@open_order = @open_orders.first # Now the database will be queried
```
###### 4.3.1.13 `collection.exists?(...)`
`collection.exists?` 方法根據指定的條件檢查關聯對象數組中是否有符合條件的對象,句法和可用選項跟 `ActiveRecord::Base.exists?` 方法一樣。
###### 4.3.1.14 `collection.build(attributes = {}, ...)`
`collection.build` 方法返回一個或多個此種關聯類型的新對象。這些對象會使用傳入的屬性初始化,還會創建對應的外鍵,但不會保存關聯對象。
```
@order = @customer.orders.build(order_date: Time.now,
order_number: "A12345")
```
###### 4.3.1.15 `collection.create(attributes = {})`
`collection.create` 方法返回一個此種關聯類型的新對象。這個對象會使用傳入的屬性初始化,還會創建對應的外鍵,只要能通過所有數據驗證,就會保存關聯對象。
```
@order = @customer.orders.create(order_date: Time.now,
order_number: "A12345")
```
###### 4.3.1.16 `collection.create!(attributes = {})`
作用和 `collection.create` 相同,但如果記錄不合法會拋出 `ActiveRecord::RecordInvalid` 異常。
##### 4.3.2 `has_many` 方法的選項
Rails 的默認設置足夠智能,能滿足常見需求。但有時還是需要定制 `has_many` 關聯的行為。定制的方法很簡單,聲明關聯時傳入選項即可。例如,下面的關聯使用了兩個選項:
```
class Customer < ActiveRecord::Base
has_many :orders, dependent: :delete_all, validate: :false
end
```
`has_many` 關聯支持以下選項:
* `:as`
* `:autosave`
* `:class_name`
* `:dependent`
* `:foreign_key`
* `:inverse_of`
* `:primary_key`
* `:source`
* `:source_type`
* `:through`
* `:validate`
###### 4.3.2.1 `:as`
`:as` 選項表明這是多態關聯。[前文](#polymorphic-associations)已經詳細介紹過多態關聯。
###### 4.3.2.2 `:autosave`
如果把 `:autosave` 選項設為 `true`,保存父對象時,會自動保存所有子對象,并把標記為析構的子對象銷毀。
###### 4.3.2.3 `:class_name`
如果另一個模型無法從關聯的名字獲取,可以使用 `:class_name` 選項指定模型名。例如,顧客有多個訂單,但表示訂單的模型是 `Transaction`,就可以這樣聲明關聯:
```
class Customer < ActiveRecord::Base
has_many :orders, class_name: "Transaction"
end
```
###### 4.3.2.4 `:dependent`
設置銷毀擁有者時要怎么處理關聯對象:
* `:destroy`:也銷毀所有關聯的對象;
* `:delete_all`:直接把所有關聯對象對數據庫中刪除,因此不會執行回調;
* `:nullify`:把外鍵設為 `NULL`,不會執行回調;
* `:restrict_with_exception`:有關聯的對象時拋出異常;
* `:restrict_with_error`:有關聯的對象時,向擁有者添加一個錯誤;
如果聲明關聯時指定了 `:through` 選項,會忽略這個選項。
###### 4.3.2.5 `:foreign_key`
按照約定,另一個模型中用來存儲外鍵的字段名是模型名后加 `_id`。`:foreign_key` 選項可以設置要使用的外鍵名:
```
class Customer < ActiveRecord::Base
has_many :orders, foreign_key: "cust_id"
end
```
不管怎樣,Rails 都不會自動創建外鍵字段,你要自己在遷移中創建。
###### 4.3.2.6 `:inverse_of`
`:inverse_of` 選項指定 `has_many` 關聯另一端的 `belongs_to` 關聯名。不能和 `:through` 或 `:as` 選項一起使用。
```
class Customer < ActiveRecord::Base
has_many :orders, inverse_of: :customer
end
class Order < ActiveRecord::Base
belongs_to :customer, inverse_of: :orders
end
```
###### 4.3.2.7 `:primary_key`
按照約定,用來存儲該模型主鍵的字段名 `id`。`:primary_key` 選項可以設置要使用的主鍵名。
假設 `users` 表的主鍵是 `id`,但還有一個 `guid` 字段。根據要求,`todos` 表中應該使用 `guid` 字段,而不是 `id` 字段。這種需求可以這么實現:
```
class User < ActiveRecord::Base
has_many :todos, primary_key: :guid
end
```
如果執行 `@user.todos.create` 創建新的待辦事項,那么 `@todo.user_id` 就是 `guid` 字段中的值。
###### 4.3.2.8 `:source`
`:source` 選項指定 `has_many :through` 關聯的關聯源名字。只有無法從關聯名種解出關聯源的名字時才需要設置這個選項。
###### 4.3.2.9 `:source_type`
`:source_type` 選項指定 `has_many :through` 關聯中用來處理多態關聯的關聯源類型。
###### 4.3.2.10 `:through`
`:through` 選項指定用來執行查詢的連接模型。`has_many :through` 關聯是實現多對多關聯的一種方式,[前文](#the-has-many-through-association)已經介紹過。
###### 4.3.2.11 `:validate`
如果把 `:validate` 選項設為 `false`,保存對象時,不會驗證關聯對象。該選項的默認值是 `true`,保存對象驗證關聯的對象。
##### 4.3.3 `has_many` 的作用域
有時可能需要定制 `has_many` 關聯使用的查詢方式,定制的查詢可在作用域代碼塊中指定。例如:
```
class Customer < ActiveRecord::Base
has_many :orders, -> { where processed: true }
end
```
在作用域代碼塊中可以使用任何一個標準的[查詢方法](active_record_querying.html)。下面分別介紹這幾個方法:
* `where`
* `extending`
* `group`
* `includes`
* `limit`
* `offset`
* `order`
* `readonly`
* `select`
* `uniq`
###### 4.3.3.1 `where`
`where` 方法指定關聯對象必須滿足的條件。
```
class Customer < ActiveRecord::Base
has_many :confirmed_orders, -> { where "confirmed = 1" },
class_name: "Order"
end
```
條件還可以使用 Hash 的形式指定:
```
class Customer < ActiveRecord::Base
has_many :confirmed_orders, -> { where confirmed: true },
class_name: "Order"
end
```
如果 `where` 使用 Hash 形式,通過這個關聯創建的記錄會自動使用 Hash 中的作用域。針對上面的例子,使用 `@customer.confirmed_orders.create` 或 `@customer.confirmed_orders.build` 創建訂單時,會自動把 `confirmed` 字段的值設為 `true`。
###### 4.3.3.2 `extending`
`extending` 方法指定一個模塊名,用來擴展關聯代理。[后文](#association-extensions)會詳細介紹關聯擴展。
###### 4.3.3.3 `group`
`group` 方法指定一個屬性名,用在 SQL `GROUP BY` 子句中,分組查詢結果。
```
class Customer < ActiveRecord::Base
has_many :line_items, -> { group 'orders.id' },
through: :orders
end
```
###### 4.3.3.4 `includes`
`includes` 方法指定使用關聯時要按需加載的間接關聯。例如,有如下的模型:
```
class Customer < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :customer
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :order
end
```
如果經常要直接獲取顧客購買的商品(`@customer.orders.line_items`),就可以把商品引入顧客和訂單的關聯中:
```
class Customer < ActiveRecord::Base
has_many :orders, -> { includes :line_items }
end
class Order < ActiveRecord::Base
belongs_to :customer
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :order
end
```
###### 4.3.3.5 `limit`
`limit` 方法限制通過關聯獲取的對象數量。
```
class Customer < ActiveRecord::Base
has_many :recent_orders,
-> { order('order_date desc').limit(100) },
class_name: "Order",
end
```
###### 4.3.3.6 `offset`
`offset` 方法指定通過關聯獲取對象時的偏移量。例如,`-> { offset(11) }` 會跳過前 11 個記錄。
###### 4.3.3.7 `order`
`order` 方法指定獲取關聯對象時使用的排序方式,用于 SQL `ORDER BY` 子句。
```
class Customer < ActiveRecord::Base
has_many :orders, -> { order "date_confirmed DESC" }
end
```
###### 4.3.3.8 `readonly`
如果使用 `readonly`,通過關聯獲取的對象就是只讀的。
###### 4.3.3.9 `select`
`select` 方法用來覆蓋獲取關聯對象數據的 SQL `SELECT` 子句。默認情況下,Rails 會讀取所有字段。
如果設置了 `select`,記得要包含主鍵和關聯模型的外鍵。否則,Rails 會拋出異常。
###### 4.3.3.10 `distinct`
使用 `distinct` 方法可以確保集合中沒有重復的對象,和 `:through` 選項一起使用最有用。
```
class Person < ActiveRecord::Base
has_many :readings
has_many :posts, through: :readings
end
person = Person.create(name: 'John')
post = Post.create(name: 'a1')
person.posts << post
person.posts << post
person.posts.inspect # => [#<Post id: 5, name: "a1">, #<Post id: 5, name: "a1">]
Reading.all.inspect # => [#<Reading id: 12, person_id: 5, post_id: 5>, #<Reading id: 13, person_id: 5, post_id: 5>]
```
在上面的代碼中,讀者讀了兩篇文章,即使是同一篇文章,`person.posts` 也會返回兩個對象。
下面我們加入 `distinct` 方法:
```
class Person
has_many :readings
has_many :posts, -> { distinct }, through: :readings
end
person = Person.create(name: 'Honda')
post = Post.create(name: 'a1')
person.posts << post
person.posts << post
person.posts.inspect # => [#<Post id: 7, name: "a1">]
Reading.all.inspect # => [#<Reading id: 16, person_id: 7, post_id: 7>, #<Reading id: 17, person_id: 7, post_id: 7>]
```
在這段代碼中,讀者還是讀了兩篇文章,但 `person.posts` 只返回一個對象,因為加載的集合已經去除了重復元素。
如果要確保只把不重復的記錄寫入關聯模型的數據表(這樣就不會從數據庫中獲取重復記錄了),需要在數據表上添加唯一性索引。例如,數據表名為 `person_posts`,我們要保證其中所有的文章都沒重復,可以在遷移中加入以下代碼:
```
add_index :person_posts, :post, unique: true
```
注意,使用 `include?` 等方法檢查唯一性可能導致條件競爭。不要使用 `include?` 確保關聯的唯一性。還是以前面的文章模型為例,下面的代碼會導致條件競爭,因為多個用戶可能會同時執行這一操作:
```
person.posts << post unless person.posts.include?(post)
```
##### 4.3.4 什么時候保存對象
把對象賦值給 `has_many` 關聯時,會自動保存對象(因為要更新外鍵)。如果一次賦值多個對象,所有對象都會自動保存。
如果無法通過驗證,隨便哪一次保存失敗了,賦值語句就會返回 `false`,賦值操作會取消。
如果父對象(`has_many` 關聯聲明所在的模型)沒保存(`new_record?` 方法返回 `true`),那么子對象也不會保存。只有保存了父對象,才會保存子對象。
如果賦值給 `has_many` 關聯時不想保存對象,可以使用 `collection.build` 方法。
#### 4.4 `has_and_belongs_to_many` 關聯詳解
`has_and_belongs_to_many` 關聯建立兩個模型之間的多對多關系。用數據庫的行話說,這種關聯的意思是有個連接數據表包含指向這兩個類的外鍵。
##### 4.4.1 `has_and_belongs_to_many` 關聯添加的方法
聲明 `has_and_belongs_to_many` 關聯后,聲明所在的類自動獲得了 16 個關聯相關的方法:
* `collection(force_reload = false)`
* `collection<<(object, ...)`
* `collection.delete(object, ...)`
* `collection.destroy(object, ...)`
* `collection=objects`
* `collection_singular_ids`
* `collection_singular_ids=ids`
* `collection.clear`
* `collection.empty?`
* `collection.size`
* `collection.find(...)`
* `collection.where(...)`
* `collection.exists?(...)`
* `collection.build(attributes = {})`
* `collection.create(attributes = {})`
* `collection.create!(attributes = {})`
這些個方法中的 `collection` 要替換成傳入 `has_and_belongs_to_many` 方法的第一個參數。`collection_singular` 要替換成第一個參數的單數形式。例如,如下的聲明:
```
class Part < ActiveRecord::Base
has_and_belongs_to_many :assemblies
end
```
每個 `Part` 模型實例都獲得了這些方法:
```
assemblies(force_reload = false)
assemblies<<(object, ...)
assemblies.delete(object, ...)
assemblies.destroy(object, ...)
assemblies=objects
assembly_ids
assembly_ids=ids
assemblies.clear
assemblies.empty?
assemblies.size
assemblies.find(...)
assemblies.where(...)
assemblies.exists?(...)
assemblies.build(attributes = {}, ...)
assemblies.create(attributes = {})
assemblies.create!(attributes = {})
```
###### 4.4.1.1 額外的字段方法
如果 `has_and_belongs_to_many` 關聯使用的連接數據表中,除了兩個外鍵之外還有其他字段,通過關聯獲取的記錄中會包含這些字段,但是只讀字段,因為 Rails 不知道如何保存對這些字段的改動。
在 `has_and_belongs_to_many` 關聯的連接數據表中使用其他字段的功能已經廢棄。如果在多對多關聯中需要使用這么復雜的數據表,可以用 `has_many :through` 關聯代替 `has_and_belongs_to_many` 關聯。
###### 4.4.1.2 `collection(force_reload = false)`
`collection` 方法返回一個數組,包含所有關聯的對象。如果沒有關聯的對象,則返回空數組。
```
@assemblies = @part.assemblies
```
###### 4.4.1.3 `collection<<(object, ...)`
`collection<<` 方法向關聯對象數組中添加一個或多個對象,并在連接數據表中創建相應的記錄。
```
@part.assemblies << @assembly1
```
這個方法與 `collection.concat` 和 `collection.push` 是同名方法。
###### 4.4.1.4 `collection.delete(object, ...)`
`collection.delete` 方法從關聯對象數組中刪除一個或多個對象,并刪除連接數據表中相應的記錄。
```
@part.assemblies.delete(@assembly1)
```
這個方法不會觸發連接記錄上的回調。
###### 4.4.1.5 `collection.destroy(object, ...)`
`collection.destroy` 方法在連接數據表中的記錄上調用 `destroy` 方法,從關聯對象數組中刪除一個或多個對象,還會觸發回調。這個方法不會銷毀對象本身。
```
@part.assemblies.destroy(@assembly1)
```
###### 4.4.1.6 `collection=objects`
`collection=` 讓關聯對象數組只包含指定的對象,根據需求會添加或刪除對象。
###### 4.4.1.7 `collection_singular_ids`
`collection_singular_ids` 返回一個數組,包含關聯對象數組中各對象的 ID。
```
@assembly_ids = @part.assembly_ids
```
###### 4.4.1.8 `collection_singular_ids=ids`
`collection_singular_ids=` 方法讓數組中只包含指定的主鍵,根據需要增刪 ID。
###### 4.4.1.9 `collection.clear`
`collection.clear` 方法刪除數組中的所有對象,并把連接數據表中的相應記錄刪除。這個方法不會銷毀關聯對象。
###### 4.4.1.10 `collection.empty?`
如果關聯數組中沒有關聯對象,`collection.empty?` 方法返回 `true`。
```
<% if @part.assemblies.empty? %>
This part is not used in any assemblies
<% end %>
```
###### 4.4.1.11 `collection.size`
`collection.size` 返回關聯對象數組中的對象數量。
```
@assembly_count = @part.assemblies.size
```
###### 4.4.1.12 `collection.find(...)`
`collection.find` 方法在關聯對象數組中查找對象,句法和可用選項跟 `ActiveRecord::Base.find` 方法一樣。同時還限制對象必須在集合中。
```
@assembly = @part.assemblies.find(1)
```
###### 4.4.1.13 `collection.where(...)`
`collection.where` 方法根據指定的條件在關聯對象數組中查找對象,但會惰性加載對象,用到對象時才會執行查詢。同時還限制對象必須在集合中。
```
@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
```
###### 4.4.1.14 `collection.exists?(...)`
`collection.exists?` 方法根據指定的條件檢查關聯對象數組中是否有符合條件的對象,句法和可用選項跟 `ActiveRecord::Base.exists?` 方法一樣。
###### 4.4.1.15 `collection.build(attributes = {})`
`collection.build` 方法返回一個此種關聯類型的新對象。這個對象會使用傳入的屬性初始化,還會在連接數據表中創建對應的記錄,但不會保存關聯對象。
```
@assembly = @part.assemblies.build({assembly_name: "Transmission housing"})
```
###### 4.4.1.16 `collection.create(attributes = {})`
`collection.create` 方法返回一個此種關聯類型的新對象。這個對象會使用傳入的屬性初始化,還會在連接數據表中創建對應的記錄,只要能通過所有數據驗證,就會保存關聯對象。
```
@assembly = @part.assemblies.create({assembly_name: "Transmission housing"})
```
###### 4.4.1.17 `collection.create!(attributes = {})`
作用和 `collection.create` 相同,但如果記錄不合法會拋出 `ActiveRecord::RecordInvalid` 異常。
##### 4.4.2 `has_and_belongs_to_many` 方法的選項
Rails 的默認設置足夠智能,能滿足常見需求。但有時還是需要定制 `has_and_belongs_to_many` 關聯的行為。定制的方法很簡單,聲明關聯時傳入選項即可。例如,下面的關聯使用了兩個選項:
```
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies, autosave: true,
readonly: true
end
```
`has_and_belongs_to_many` 關聯支持以下選項:
* `:association_foreign_key`
* `:autosave`
* `:class_name`
* `:foreign_key`
* `:join_table`
* `:validate`
* `:readonly`
###### 4.4.2.1 `:association_foreign_key`
按照約定,在連接數據表中用來指向另一個模型的外鍵名是模型名后加 `_id`。`:association_foreign_key` 選項可以設置要使用的外鍵名:
`:foreign_key` 和 `:association_foreign_key` 這兩個選項在設置多對多自連接時很有用。
```
class User < ActiveRecord::Base
has_and_belongs_to_many :friends,
class_name: "User",
foreign_key: "this_user_id",
association_foreign_key: "other_user_id"
end
```
###### 4.4.2.2 `:autosave`
如果把 `:autosave` 選項設為 `true`,保存父對象時,會自動保存所有子對象,并把標記為析構的子對象銷毀。
###### 4.4.2.3 `:class_name`
如果另一個模型無法從關聯的名字獲取,可以使用 `:class_name` 選項指定模型名。例如,一個部件由多個裝配件組成,但表示裝配件的模型是 `Gadget`,就可以這樣聲明關聯:
```
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies, class_name: "Gadget"
end
```
###### 4.4.2.4 `:foreign_key`
按照約定,在連接數據表中用來指向模型的外鍵名是模型名后加 `_id`。`:foreign_key` 選項可以設置要使用的外鍵名:
```
class User < ActiveRecord::Base
has_and_belongs_to_many :friends,
class_name: "User",
foreign_key: "this_user_id",
association_foreign_key: "other_user_id"
end
```
###### 4.4.2.5 `:join_table`
如果默認按照字典順序生成的默認名不能滿足要求,可以使用 `:join_table` 選項指定。
###### 4.4.2.6 `:validate`
如果把 `:validate` 選項設為 `false`,保存對象時,不會驗證關聯對象。該選項的默認值是 `true`,保存對象驗證關聯的對象。
##### 4.4.3 `has_and_belongs_to_many` 的作用域
有時可能需要定制 `has_and_belongs_to_many` 關聯使用的查詢方式,定制的查詢可在作用域代碼塊中指定。例如:
```
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies, -> { where active: true }
end
```
在作用域代碼塊中可以使用任何一個標準的[查詢方法](active_record_querying.html)。下面分別介紹這幾個方法:
* `where`
* `extending`
* `group`
* `includes`
* `limit`
* `offset`
* `order`
* `readonly`
* `select`
* `uniq`
###### 4.4.3.1 `where`
`where` 方法指定關聯對象必須滿足的條件。
```
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies,
-> { where "factory = 'Seattle'" }
end
```
條件還可以使用 Hash 的形式指定:
```
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies,
-> { where factory: 'Seattle' }
end
```
如果 `where` 使用 Hash 形式,通過這個關聯創建的記錄會自動使用 Hash 中的作用域。針對上面的例子,使用 `@parts.assemblies.create` 或 `@parts.assemblies.build` 創建訂單時,會自動把 `factory` 字段的值設為 `"Seattle"`。
###### 4.4.3.2 `extending`
`extending` 方法指定一個模塊名,用來擴展關聯代理。[后文](#association-extensions)會詳細介紹關聯擴展。
###### 4.4.3.3 `group`
`group` 方法指定一個屬性名,用在 SQL `GROUP BY` 子句中,分組查詢結果。
```
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies, -> { group "factory" }
end
```
###### 4.4.3.4 `includes`
`includes` 方法指定使用關聯時要按需加載的間接關聯。
###### 4.4.3.5 `limit`
`limit` 方法限制通過關聯獲取的對象數量。
```
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies,
-> { order("created_at DESC").limit(50) }
end
```
###### 4.4.3.6 `offset`
`offset` 方法指定通過關聯獲取對象時的偏移量。例如,`-> { offset(11) }` 會跳過前 11 個記錄。
###### 4.4.3.7 `order`
`order` 方法指定獲取關聯對象時使用的排序方式,用于 SQL `ORDER BY` 子句。
```
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies,
-> { order "assembly_name ASC" }
end
```
###### 4.4.3.8 `readonly`
如果使用 `readonly`,通過關聯獲取的對象就是只讀的。
###### 4.4.3.9 `select`
`select` 方法用來覆蓋獲取關聯對象數據的 SQL `SELECT` 子句。默認情況下,Rails 會讀取所有字段。
###### 4.4.3.10 `uniq`
`uniq` 方法用來刪除集合中重復的對象。
##### 4.4.4 什么時候保存對象
把對象賦值給 `has_and_belongs_to_many` 關聯時,會自動保存對象(因為要更新外鍵)。如果一次賦值多個對象,所有對象都會自動保存。
如果無法通過驗證,隨便哪一次保存失敗了,賦值語句就會返回 `false`,賦值操作會取消。
如果父對象(`has_and_belongs_to_many` 關聯聲明所在的模型)沒保存(`new_record?` 方法返回 `true`),那么子對象也不會保存。只有保存了父對象,才會保存子對象。
如果賦值給 `has_and_belongs_to_many` 關聯時不想保存對象,可以使用 `collection.build` 方法。
#### 4.5 關聯回調
普通回調會介入 Active Record 對象的生命周期,在很多時刻處理對象。例如,可以使用 `:before_save` 回調在保存對象之前處理對象。
關聯回調和普通回調差不多,只不過由集合生命周期中的事件觸發。關聯回調有四種:
* `before_add`
* `after_add`
* `before_remove`
* `after_remove`
關聯回調在聲明關聯時定義。例如:
```
class Customer < ActiveRecord::Base
has_many :orders, before_add: :check_credit_limit
def check_credit_limit(order)
...
end
end
```
Rails 會把添加或刪除的對象傳入回調。
同一事件可觸發多個回調,多個回調使用數組指定:
```
class Customer < ActiveRecord::Base
has_many :orders,
before_add: [:check_credit_limit, :calculate_shipping_charges]
def check_credit_limit(order)
...
end
def calculate_shipping_charges(order)
...
end
end
```
如果 `before_add` 回調拋出異常,不會把對象加入集合。類似地,如果 `before_remove` 拋出異常,對象不會從集合中刪除。
#### 4.6 關聯擴展
Rails 基于關聯代理對象自動創建的功能是死的,但是可以通過匿名模塊、新的查詢方法、創建對象的方法等進行擴展。例如:
```
class Customer < ActiveRecord::Base
has_many :orders do
def find_by_order_prefix(order_number)
find_by(region_id: order_number[0..2])
end
end
end
```
如果擴展要在多個關聯中使用,可以將其寫入具名擴展模塊。例如:
```
module FindRecentExtension
def find_recent
where("created_at > ?", 5.days.ago)
end
end
class Customer < ActiveRecord::Base
has_many :orders, -> { extending FindRecentExtension }
end
class Supplier < ActiveRecord::Base
has_many :deliveries, -> { extending FindRecentExtension }
end
```
在擴展中可以使用如下 `proxy_association` 方法的三個屬性獲取關聯代理的內部信息:
* `proxy_association.owner`:返回關聯所屬的對象;
* `proxy_association.reflection`:返回描述關聯的反射對象;
* `proxy_association.target`:返回 `belongs_to` 或 `has_one` 關聯的關聯對象,或者 `has_many` 或 `has_and_belongs_to_many` 關聯的關聯對象集合;
### 反饋
歡迎幫忙改善指南質量。
如發現任何錯誤,歡迎修正。開始貢獻前,可先行閱讀[貢獻指南:文檔](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)。
翻譯如有錯誤,深感抱歉,歡迎 [Fork](https://github.com/ruby-china/guides/fork) 修正,或至此處[回報](https://github.com/ruby-china/guides/issues/new)。
文章可能有未完成或過時的內容。請先檢查 [Edge Guides](http://edgeguides.rubyonrails.org) 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 [Ruby on Rails 指南準則](ruby_on_rails_guides_guidelines.html)來了解行文風格。
最后,任何關于 Ruby on Rails 文檔的討論,歡迎到 [rubyonrails-docs 郵件群組](http://groups.google.com/group/rubyonrails-docs)。
- Ruby on Rails 指南 (651bba1)
- 入門
- Rails 入門
- 模型
- Active Record 基礎
- Active Record 數據庫遷移
- Active Record 數據驗證
- Active Record 回調
- Active Record 關聯
- Active Record 查詢
- 視圖
- Action View 基礎
- Rails 布局和視圖渲染
- 表單幫助方法
- 控制器
- Action Controller 簡介
- Rails 路由全解
- 深入
- Active Support 核心擴展
- Rails 國際化 API
- Action Mailer 基礎
- Active Job 基礎
- Rails 程序測試指南
- Rails 安全指南
- 調試 Rails 程序
- 設置 Rails 程序
- Rails 命令行
- Rails 緩存簡介
- Asset Pipeline
- 在 Rails 中使用 JavaScript
- 引擎入門
- Rails 應用的初始化過程
- Autoloading and Reloading Constants
- 擴展 Rails
- Rails 插件入門
- Rails on Rack
- 個性化Rails生成器與模板
- Rails應用模版
- 貢獻 Ruby on Rails
- Contributing to Ruby on Rails
- API Documentation Guidelines
- Ruby on Rails Guides Guidelines
- Ruby on Rails 維護方針
- 發布記
- A Guide for Upgrading Ruby on Rails
- Ruby on Rails 4.2 發布記
- Ruby on Rails 4.1 發布記
- Ruby on Rails 4.0 Release Notes
- Ruby on Rails 3.2 Release Notes
- Ruby on Rails 3.1 Release Notes
- Ruby on Rails 3.0 Release Notes
- Ruby on Rails 2.3 Release Notes
- Ruby on Rails 2.2 Release Notes