<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # 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 關聯](https://box.kancloud.cn/2016-05-11_5732ab22d98e9.png) 在 `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 ``` ![has_one 關聯](https://box.kancloud.cn/2016-05-11_5732ab22ee4fd.png) 相應的遷移如下: ``` 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` 關聯時,另一個模型使用復數形式。 ![has_many 關聯](https://box.kancloud.cn/2016-05-11_5732ab2312add.png) 相應的遷移如下: ``` 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 ``` ![has_many :through 關聯](https://box.kancloud.cn/2016-05-11_5732ab232f0f3.png) 相應的遷移如下: ``` 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 ``` ![has_one :through 關聯](https://box.kancloud.cn/2016-05-11_5732ab234cf34.png) 相應的遷移如下: ``` 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 ``` ![has_and_belongs_to_many 關聯](https://box.kancloud.cn/2016-05-11_5732ab2368bbd.png) 相應的遷移如下: ``` 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 ``` ![多態關聯](https://box.kancloud.cn/2016-05-11_5733309e83ca3.png) #### 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”前面。 模型名的順序使用字符串的 `&lt;` 操作符確定。所以,如果兩個字符串的長度不同,比較最短長度時,兩個字符串是相等的,但長字符串的排序比短字符串靠前。例如,你可能以為“"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&lt;&lt;(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&lt;&lt;(object, ...)` `collection&lt;&lt;` 方法向關聯對象數組中添加一個或多個對象,并把各所加對象的外鍵設為調用此方法的模型的主鍵。 ``` @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` 方法指定通過關聯獲取對象時的偏移量。例如,`-&gt; { 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&lt;&lt;(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&lt;&lt;(object, ...)` `collection&lt;&lt;` 方法向關聯對象數組中添加一個或多個對象,并在連接數據表中創建相應的記錄。 ``` @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` 方法指定通過關聯獲取對象時的偏移量。例如,`-&gt; { 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 id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看