# 4.3 模型中的關聯關系(Relations)
## 概要:
本課時講解 Rails 中 Model 和 Model 間的關聯關系。
## 知識點:
1. belongs_to
1. has_one
1. has_many
1. has_and_belongs_to_many
1. self join
## 正文
### 導讀
如果你對一對一關系,一對多關系,多對多關系并不十分了解的話,或者你對關系型數據庫并不十分了解的話,建議你在閱讀下面的內容前,先熟悉一下相關內容。因為我并不想照本宣科的講解手冊。我想講的,是對它的理解,并且把我們的精力,放到設計我們的商城中。
本章涉及的知識,可以查看 [Active Record Associations](http://guides.rubyonrails.org/association_basics.html),或者 [ActiveRecord::Associations::ClassMethods](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html)。
接下來的內容,希望能幫助你理解模型間的關聯關系。
### 4.3.1 模型間的關系
在前面的章節里,我們為商城設計了界面,并且使用了3個 model:
1. User,網站用戶,使用 devise 提供了用戶注冊,登錄功能。
1. Product,商品
1. Variant,商品類型
我們在前面講解的過程中,已經提到了 Product 和 Variant 的關系。一個 Product 有多個 Variant。現在我們需要增加幾個模型,模型是根據功能來的,我們的網店要增加哪些功能呢?
- 當用戶購買實物商品的時候,我們是要輸入它的收貨地址(Address)。
- 當用戶選擇商品的時候,選擇不同的顏色和大小,會有不同的價格(Variant)。
- 我們點擊購買,會創建一個購物訂單(Order),上面有我們選擇的商品,應支付的金額,和訂單的狀態。
- 查看用戶購買的商品類型
在我們的網店里,一個 User 有一個地址,每次購物的時候,會讀取這個地址作為送貨地址。
一個 Product 有多個 Variant,每個 Variant 保存它的顏色,大小等屬性。
一個用戶會有多個訂單 Order,每個訂單會顯示購買的商品 Product,以及多條購買記錄,每條記錄顯示購買的 Variant 的每個數量和應付的價格,這里我們使用 LineItem 表示訂單的訂單項。
### 4.3.2 外鍵
兩個 model 之間,通過外鍵進行關聯,Rails 中默認的外鍵名稱是所屬 model 的 `名稱_id`,比如,User 有一條 Address 記錄,那么 addresses 表上,需要增加一個數字類型的字段 `user_id`。而 User 的主鍵通常為 id 字段。有一些遺留的數據庫,使用的外鍵可能不是按照 Rails 默認的格式,所以在聲明外鍵關聯時,需要指定 `foreign_key`。
在我們創建 Model 的時候,可以在 generate 命令上增加外鍵關聯,我們現在創建 Address 這個 Model
~~~
rails g model address user:references state city address address2 zipcode receiver phone
~~~
在創建的 migration 文件中:
~~~
create_table :addresses do |t|
t.references :user, index: true, foreign_key: true
~~~
自動增加了外鍵關聯,并且將 user_id 加入索引。如果是更改其他數據庫,需要在 migration 文件內單獨設置索引:
~~~
add_index "addresses", ["user_id"], name: "index_addresses_on_user_id"
~~~
模型間的關系,都是通過外鍵實現的,下面我們詳細介紹模型間的關系,并且實現我們商城的 Model。
### 4.3.3 一對一關系
一對一關系的設定,再一次體現了 Rails 在開發中的便捷:
~~~
class User < ActiveRecord::Base
has_one :address
end
class Address < ActiveRecord::Base
belongs_to :user
end
~~~
在一對一關系中,`belongs_to :user` 中,`:user` 是單數,`has_one :address` 中,`:address` 也是單數。
我們進入到 console 里來測試一下:
~~~
user = User.first
user.address
=> nil
~~~
#### 4.3.3.1 新建子資源
如何為 user 保存 address 呢?
一種是使用 Address 的類方法 `create`:
~~~
Address.create(user_id: user.id, ...)
~~~
我們也可以省去 id 的寫法,直接寫上所屬的實例:
~~~
Address.create(user: user, ...)
~~~
一種是使用實例方法:
~~~
address = Address.new
address.user = user
address.save
~~~
或者:
~~~
user.address = Address.create( ... )
~~~
這種方法會產生兩句 SQL,先是 insert 一個 address 到數據庫,然后更新它的 user_id 為剛才的 user。我們可以換一個方法:
~~~
user.address = Address.new( ... )
~~~
它只產生一條 insert SQL,并且會帶上 user_id 的值。
在創建關聯關系時,還有這樣的方法:
~~~
user.create_address( ... )
user.build_address( ... )
~~~
build_xxx 相當于 Address.new。create_xxx也會產生兩條 SQL,每條 SQL 都包含在一個 transaction 中。
所以我們得出結論:
把一個未保存的實例,賦值給一對一關系時,它會自動保存,并且只有一條 sql 產生。
先 create 一個實例,再把賦值給一對一關系時,是先保存,再更新,產生兩條 sql。
#### 4.3.3.2 保存子資源
當我們編寫表單的時候,一個表單針對的是一個資源。當這個資源擁有(has_one 或 has_many)子資源時,我們可以在提交表單的時候,將它擁有的資源也保存到數據庫中。
這時,我們需要在 User中,做一個聲明:
~~~
class User < ActiveRecord::Base
has_one :address
accepts_nested_attributes_for :address
end
~~~
`accepts_nested_attributes_for` 會為 User 增加一個新的方法 `address_attributes=(attributes)`,這樣,在創建 User 的 時候:
~~~
user_hash = { email: "test@123.com", password: "123456", password_confirmation: "123456", address_attributes: { receiver: "Some One", state: "Beijing", city: "Beijing", phone: "123456"} }
u = User.create(user_hash)
u.address
~~~
只要保存 User 的時候,傳遞入 Address 的參數,就可以把關聯的 address 一并保存到數據庫中了。
更新記錄的時候,也可以使用同樣的方法:
~~~
user_hash = { email: "changed@123.com", address_attributes: { receiver: "Other One" } }
user.update(user_hash)
~~~
但是,這里要注意,上面的方法會把之前舊記錄的 user_id 設為 nil,然后插入一條新的記錄。這并不能真正起到更新的作用,除非所有屬性都重新復制,不然,新的 address 記錄只有 receiver 這個值。
我們在 accepts_nested_attributes_for 后增加一個參數:
~~~
accepts_nested_attributes_for :address, update_only: true
~~~
這樣,update 時候會更新已有的記錄。
如果我們不能增加 `update_only` 屬性,為了避免創建無用的記錄,需要在 hash 里指定子資源的 id:
~~~
user_hash = { email: "changed@123.com", address_attributes: { id: 1, receiver: "Other One" } }
user.update(user_hash)
~~~
#### 4.3.3.3 使用表單保存子資源
`accepts_nested_attributes_for` 方法,在 Form 中有其對應的方法:
~~~
<%= f.fields_for :address do |address_form| %>
<%= address_form.hidden_field :id unless resource.new_record? %>
<div class="form-group">
<%= address_form.label :state, class: "control-label" %><br />
<%= address_form.text_field :state, class: "form-control" %>
</div>
...
<% end %>
~~~
打開 [代碼](https://github.com/liwei78/rails-practice-code/blob/master/chapter_4/shop/app/views/devise/registrations/edit.html.erb#L32),在編輯一個用戶的時候,我為它增加了一個 `f.fields_for` 的子表單,對應了子資源的屬性。
我想,這段代碼這并不難理解,不過我們用了 Devise 這個 gem,還需要做一點額外的處理。
打開 application_controller.rb,我們需要讓 devise 支持傳進來新增的參數:
~~~
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:email, :password, :password_confirmation, :address_attributes) }
devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:email, :password, :password_confirmation, :current_password, address_attributes: [:state, :city, :address, :address2, :zipcode, :receiver, :phone] ) }
end
end
~~~
在我們注冊賬號的時候,并沒有創建 address ,但是在編輯的時候,因為它是 nil,所以不會顯示這個子表單,所以我們需要在編輯的時候創建一個空的 address:
`views/devise/registrations/edit.html.erb`
~~~
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<% resource.build_address if resource.address.nil? %>
...
~~~
當然,我們也可以在注冊的時候提供地址表單,大家不妨一試。
#### 4.3.3.4 刪除關聯的子資源
在上一節里,我們介紹了 delete 和 destroy 方法,我們可以使用這兩個方法把關聯的 address 刪除掉:
~~~
u.address.delete
SQL (10.0ms) DELETE FROM "addresses" WHERE "addresses"."id" = ? [["id", 2]]
~~~
或者:
~~~
u.address.destroy
(0.1ms) begin transaction
SQL (0.7ms) DELETE FROM "addresses" WHERE "addresses"."id" = ? [["id", 3]]
(9.2ms) commit transaction
~~~
兩者的區別在上一節介紹過,我們注意到,delete 直接發送數據庫刪除命令,而 destroy 會將刪除命令放置到一個 sql 的事物中,因為它會觸發模型中的回調,如果回調拋出異常,刪除動作會失敗。
#### 4.3.3.5 刪除自身同時刪除關聯的子資源
在刪除某個資源的時候,我們想把它擁有的資源一并刪除,這時,我們需要給 has_one 方法,增加一個參數:
~~~
has_one :address, dependent: :destroy
~~~
dependent 可以接收五個參數:
| 參數 | 含義 |
|-----|-----|
| :destroy | 刪除擁有的資源 |
| :delete | 直接發送刪除命令,不會執行回調 |
| :nullify | 將擁有的資源外鍵設為 null |
| :restrict_with_exception | 如果擁有資源,會拋出異常,也就是說,當它 has_one 為 nil 的時候,才能正常刪除它自己 |
| :restrict_with_error | 如有擁有資源,會增加一個 errors 信息。 |
在 belongs_to 上,也可以設置 dependent,但它只有兩個參數:
| 參數 | 含義 |
|-----|-----|
| :destroy | 刪除它所屬的資源 |
| :delete | 刪除它所屬的資源,直接發送刪除命令,不會執行回調 |
兩種設定,出發角度是不同的,不過,刪除本身的同時刪除上層資源是比較危險的,需謹慎。
#### 4.3.3.6 失去關聯關系的子資源
如果在 has_one 中設置了 `dependent: :destroy` 或 `dependent: :delete`,當子資源失去該關聯關系時,它也會被刪除。
~~~
user.address = nil
~~~
如果不設置,一個子資源失去關系時,外鍵設置為 null。
#### 4.3.3.7 子資源維護
當一個子資源失去關聯關系,和它在關聯關系中被刪除,是一樣的。我們在設計時,應盡量避免產生孤立的記錄,這些記錄外鍵為 null,或者所屬的資源已經被刪除,他們是無意義的存在。
### 4.3.4 一對多關系
在電商系統里,一個用戶是有多個訂單(Order)的,User 中使用的是 has_many 方法:
~~~
class User < ActiveRecord::Base
has_many :orders
end
~~~
除了名稱變為復數形式,返回的結果是數組,其他情形和“一對一”是一樣的。
我們使用 generate 創建 Order:
~~~
rails g model order user:references number payment_state shipment_state
~~~
number 是訂單的唯一編號,payment_state 是付款狀態,shipment_state 是發貨狀態。
payment_state 的狀態順序是:pending(等待支付),paid(已支付)。
shipment_state 的狀態順序是:pending(等待發貨),shipped(已發貨)。
這兩種狀態,我們只做簡單的設計,實際中要復雜得多。
開源電商程序 [spree](https://spreecommerce.com/) 是一套很好的在線交易程序,因為其開源,其中的概念和定義對開發電商程序有很好的啟發。它的源代碼在 [這里](https://github.com/spree/spree),目前是最新版本是 3.0.2.beta。
#### 4.3.4.1 添加子資源
一對多關系返回的,是 [CollectionProxy](http://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html) 實例。
當添加一對多關系時,可以很“形象”的使用:
~~~
product.variants << Variant.new
product.variants << [Variant.new, Variant.new]
~~~
執行 `<<` 的時候,variant 的 product_id 會自動保存為 product.id。
如果 variant 是一個未保存到數據庫的實例,<< 執行的時候會自動將它保存,并且賦予它 product_id 值。這是一步完成的,只有一條 SQL。
但是,如果是下面的情形:
~~~
product.variants << Variant.create
~~~
會把 variant 先保存到數據庫,然后再更新它的 product_id 字段,這會產生兩條 SQL。
這里也可以使用 build 方法,和上面“一對一關系”不同的是,它需要在 collection 上執行:
~~~
variant = product.variants.build( ... )
variant.save
~~~
build 返回的是一個未保存的實例。查看 `product.variants`,會看到它包含了一個未保存的 variant(ID 為 nil)。
另一種情形:
~~~
product.variants.build( ... )
product.save
~~~
當這個 product.save 的時候,這個 variant 也會保存到數據庫中。
#### 4.3.4.2 刪除子資源
刪除資源的時候,可以使用幾個方法:
~~~
product.variants.delete(...)
product.variants.destroy(...)
product.variants.clear
~~~
delete 不會真正刪除掉資源,而是把它的外鍵(product_id)設為 nil,而 destroy 會真正的刪除掉它并出發回調。
他們都可以傳遞進一個實例,或者實例的集合,而并不管這個實例是否真的屬于它。
~~~
product.variants.delete(Variant.find(1))
product.variants.delete(Variant.find(1,2,3))
~~~
這樣是不是太霸道了?所以,建議用第三個方法更穩妥些。clear 方法會把外鍵置為 nil。
如果再 has_many 上聲明了 `dependent: :destroy`,會用 destroy 方式把它們刪除(有回調)。如果聲明的是 `dependent: :delete_all`,會用 delete 方法(跳過回調)。這和一對一中描述是一致的。
注意:
has_many 和 has_one 上的 dependent 選項,適用以下兩種情形:
- 刪除自身時,如何處理子資源
- 當子資源失去該關聯關系時,如何處理該子資源
我們來看下一節。
#### 4.3.4.3 更改子資源
當改動關系的時候,可以直接使用 `=`,假設我們有 ID 為 1,2,3,4 的 Variant:
~~~
product.variants = Variant.find(1,2)
~~~
這時會自動把 ID:1,ID:2 的 product_id 外鍵設為 null。
再次選擇 ID:3,ID:4 的 variant:
~~~
product.variants = Variant.find(3,4)
~~~
會自動把 ID:3,ID:4 的 product_id 外鍵設置為 product.id。
如果在 has_many 設置了 `dependent: :destroy`,當 UD:1 和 ID:2 失去關聯的時候,會把它們從數據庫中刪除掉。這與 has_one 中的 dependent 選項是一樣的。詳見本章前面 `4.3.3.4 刪除自身同時刪除關聯的子資源`。
#### 4.3.4.4 counter_cache
“一對多”關系中,`belongs_to` 方法可以增加 counter_cache 屬性:
~~~
class Order < ActiveRecord::Base
belongs_to :user, counter_cache: true
end
~~~
這時,我們需要給 users 表增加一個字段:orders_count,當我們把一個 order 保存到一對多的關系中時,orders_count 會自動 +1,當把一個資源從關系中刪除,該字段會 -1。如此我們不必去增加計算一個 user 有多少個 orders,只需要讀該字段就可以了。
向 Users 表添加 orders_count 字段:
~~~
rails g migration add_orders_count_to_users orders_count:integer
~~~
#### 4.3.4.5 多態
當一個資源可能屬于多種資源時,可以用到多態。舉個栗子:
商品可以評論,文章可以評論,而評論 model 對任何一個資源都是一樣的功能,所以,評論在 belongs_to 的后面,增加:
~~~
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
~~~
Comment 的遷移文件,也相應的增加設定:
~~~
t.references :commentable, polymorphic: true, index: true
~~~
如果是手動添加字段,需要這樣來寫:
~~~
t.string :commentable_type
t.integer :commentable_id
~~~
說明,查找一個多態資源時,是根據擁有者的類型(type,一般是它的類名稱)和 ID 進行匹配的。
擁有評論的 model,也需要改動下:
~~~
class Product < ActiveRecord::Base
has_many :commentable, as: :commentable
end
class Topic < ActiveRecord::Base
has_many :commentable, as: :commentable
end
~~~
多態并不局限于一對多關系,一對一也同樣適用。
### 4.3.5 中間模型和中間表
has_one 和 has_many,是兩個 model 間的操作。我們可以增加一個中間模型,描述之前兩個 model間的關系。
### 4.3.5.1 中間模型
我們先創建訂單項(LineItem)這個 model,它屬于一個訂單,也屬于一個商品類型(Variant)。
~~~
rails g model line_item order:references variant:references quantity:integer
~~~
對于一個訂單,我們有多個訂單項,對于一個訂單項,會關聯購買的具體商品類型,那么,一個訂單擁有的商品類型,就可以通過 through 查找到。
~~~
class Order < ActiveRecord::Base
belongs_to :user, counter_cache: true
has_many :line_items
has_many :variants, through: :line_items
end
~~~
~~~
class LineItem < ActiveRecord::Base
belongs_to :order
belongs_to :variant
end
~~~
我們進到終端里進行查找:
~~~
order = Order.first
order.variants
=> SELECT "variants".* FROM "variants" INNER JOIN "line_items" ON "variants"."id" = "line_items"."variant_id" WHERE "line_items"."order_id" = ? [["order_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy []>
~~~
可以看到,through 為使用了 `inner join` 的 sql 語法。
LineItem 是兩個模型,Order 和 Variant 的中間模型,它表示訂單中的每一項。但是,中間模型不一定要使用兩個 `belongs_to` 連接兩邊的模型,比如:
~~~
class User < ActiveRecord::Base
has_many :orders
has_many :line_items, through: :orders
end
~~~
進到終端,我們查看一個用戶有哪些訂單項:
~~~
user = User.first
user.line_items
=> SELECT "line_items".* FROM "line_items" INNER JOIN "orders" ON "line_items"."order_id" = "orders"."id" WHERE "orders"."user_id" = ? [["user_id", 1]]
~~~
從左邊可以查到右邊資源,那么,可以通過中間表,從右邊查找左邊資源么?
我們給 Variant 增加關聯:
~~~
class Variant < ActiveRecord::Base
belongs_to :product
has_many :line_item
has_many :orders, through: :line_item
end
~~~
進入終端:
~~~
v = Variant.last
v.orders
=> SELECT "orders".* FROM "orders" INNER JOIN "line_items" ON "orders"."id" = "line_items"."order_id" WHERE "line_items"."variant_id" = ? [["variant_id", 2]]
~~~
因為中間表 LineItem 擁有兩邊的外鍵,所以可以查找 variant 的 orders。但是 orders 上沒有 line_item_id 字段,因為這不符合我們的業務邏輯,所以無法查找 line_item.user。如果需要查找,可以給 line_item 上增加 user_id 字段。
~~~
class LineItem < ActiveRecord::Base
belongs_to :order
belongs_to :variant
belongs_to :user
end
~~~
### 4.3.5.2 中間表
中間模型的作用,除了連接兩端模型外,更重要的是,它保存了業務中屬于中間模型的數據,比如,訂單項中的 quantity 字段。如果模型不必或者沒有這種字段,可以不用增加 model,而直接使用中間表。
我們有一個功能:保存用戶購買的商品類型。這時可以使用中間表,保存購買關系。
中間表具有兩端模型的外鍵。兩端模型使用 `has_and_belongs_to_many` 方法(簡寫:HABTM)。
在創建中間表的時候,也可以使用 migration,如果在表名中包含 `JoinTable` 字樣,會自動創建中間表:
~~~
rails g migration CreateJoinTable users variants:uniq
~~~
運行 `rake db:migrate`,查看 schema.rb:
~~~
create_table "users_variants", id: false, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "variant_id", null: false
end
add_index "users_variants", ["variant_id", "user_id"], name: "index_users_variants_on_variant_id_and_user_id", unique: true
~~~
調整一下 User 和 Variant model:
~~~
class User < ActiveRecord::Base
...
has_and_belongs_to_many :variants
end
class Variant < ActiveRecord::Base
...
has_and_belongs_to_many :users
end
~~~
在終端里測試:
~~~
user.variants
=> SELECT "variants".* FROM "variants" INNER JOIN "users_variants" ON "variants"."id" = "users_variants"."variant_id" WHERE "users_variants"."user_id" = ? [["user_id", 1]]
variant.users
=> SELECT "users".* FROM "users" INNER JOIN "users_variants" ON "users"."id" = "users_variants"."user_id" WHERE "users_variants"."variant_id" = ? [["variant_id", 2]]
~~~
利用中間表,實現了多對多關系。
### 4.3.5.3 多對多關系
查看一個用戶購買了哪些商品類型,和查看一個商品類型被哪些用戶購買,這就是多對多關系。
保存和刪除多對多關系,和一對多關系的操作是一樣的。因為我們在創建 migration 時,增加了索引唯一校驗,在操作時要做好異常處理,或者保存前進行判斷。
~~~
user.variants << variant
user.variants << variant
=> SQLite3::ConstraintException: columns variant_id, user_id are not unique: ...
~~~
### 4.3.5.4 inner join
ActiveRecord 在查詢關聯關系時,使用的是 inner join 查詢,我們可以單獨使用 `join` 方法,實現該查詢。
比如,一個簡單的 join 查詢:
~~~
% Order.joins(:line_items)
=> SELECT "orders".* FROM "orders" INNER JOIN "line_items" ON "line_items"."order_id" = "orders"."id"
~~~
也可以查詢多個關聯的:
~~~
% Order.joins(:line_items, :user)
=> SELECT "orders".* FROM "orders" INNER JOIN "line_items" ON "line_items"."order_id" = "orders"."id" INNER JOIN "users" ON "users"."id" = "orders"."user_id"
~~~
或者嵌套關聯:
~~~
% Order.joins(line_items: [:variant])
=> SELECT "orders".* FROM "orders" INNER JOIN "line_items" ON "line_items"."order_id" = "orders"."id" INNER JOIN "variants" ON "variants"."id" = "line_items"."variant_id"
~~~
但是,在一些更復雜的查詢中,我們需要改變 `inner join` 查詢為 `left join` 或 `right join`:
~~~
User.select("users.*, orders.*").joins("LEFT JOIN `orders` ON orders.user_id = users.id")
~~~
這時返回的是全部用戶,即便它沒有訂單。這在生成一些報表時是有用的。
### 4.3.6 自連接
在設計模型的時候,一個模型即可以是 Catalog(類別),也可以是 Subcatalog(子類別),我們為網店添加 `類別` Model:
~~~
rails g model catalog parent_catalog:references name parent:boolean
~~~
看一下 catalog.rb:
~~~
class Catalog < ActiveRecord::Base
has_many :subcatalogs, class_name: "Catalog", foreign_key: "parent_catalog_id"
belongs_to :parent_catalog, class_name: "Catalog"
has_many :products
end
~~~
這樣,我們可以實現分類,也可以吧商品加入到某個分類中。
### 4.3.7 雙向關聯
我們查找關聯關系的時候,是可以在兩邊同時查找,比如:
~~~
class User < ActiveRecord::Base
has_one :address
end
class Address < ActiveRecord::Base
belongs_to :user
end
~~~
我們可以 `user.address`,也可以 `address.user`,這叫做 Bi-directional,雙向關聯。(和它相反,Uni-directional,單向關聯)
但是,這在我們的內存查找中,會引起問題:
~~~
u = User.first
a = u.address
u.email == a.user.email
=> true
u.email = "a@1.com"
u.email == a.user.email
=> false
~~~
原因是:
~~~
u.object_id
=> 70241969456560
a.user.object_id
=> 70241969637580
~~~
兩個類并不是在內存中指向同一個地址,他們是不同的兩個類。
為了避免這個問題,我們需要使用 inverse_of:
~~~
class User < ActiveRecord::Base
has_one :address, inverse_of: :user
end
class Address < ActiveRecord::Base
belongs_to :user, inverse_of: :address
end
~~~
當 model 的關聯關系上,已經有 polymorphic,through,as 時,可以不用加 inverse_of,它自然會指向同一個 object,大家可以使用 user 和 order 之間的關聯驗證。對于 user 和 address 之間,還是應該加上 inverse_of 選項。
### 4.3.8 Rspec測試
關聯關系的測試,可以使用 [shoulda-matchers](https://github.com/thoughtbot/shoulda-matchers) 這個 gem。它為 Rails 的模型間關聯提供了方便的測試方法。
比如:
~~~
RSpec.describe User, type: :model do
it { should have_many(:orders) }
end
RSpec.describe Order, type: :model do
it { should belong_to(:user) }
end
~~~
更多模型間關聯關系測試的方法,可以查看 [ActiveRecord matchers](https://github.com/thoughtbot/shoulda-matchers#activerecord-matchers)
- 寫在前面
- 第一章 Ruby on Rails 概述
- Ruby on Rails 開發環境介紹
- Rails 文件簡介
- 用戶界面(UI)設計
- 第二章 Rails 中的資源
- 應用 scaffold 命令創建資源
- REST 架構
- 深入路由(routes)
- 第三章 Rails 中的視圖
- 布局和輔助方法
- 表單
- 視圖中的 AJAX 交互
- 模板引擎的使用
- 第四章 Rails 中的模型
- 模型的基礎操作
- 深入模型查詢
- 模型中的關聯關系
- 模型中的校驗
- 模型中的回調
- 第五章 Rails 中的控制器
- 控制器中的方法
- 控制器中的邏輯
- 第六章 Rails 的配置及部署
- Assets 管理
- 緩存及緩存服務
- 異步任務及郵件發送
- I18n
- 生產環境部署
- 常用 Gem
- 寫在后面