在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ Ruby/ 4.3 模型中的關(guān)聯(lián)關(guān)系(Relations)
寫在后面
寫在前面
第六章 Rails 的配置及部署
第四章 Rails 中的模型
4.4 模型中的校驗(Validates)
1.3 用戶界面(UI)設(shè)計
6.5 生產(chǎn)環(huán)境部署
3.2 表單
4.3 模型中的關(guān)聯(lián)關(guān)系(Relations)
4.5 模型中的回調(diào)(Callback)
第五章 Rails 中的控制器
4.2 深入模型查詢
5.2 控制器中的方法
6.2 緩存
3.4 模板引擎的使用
6.4 I18n
第一章 Ruby on Rails 概述
6.6 常用 Gem
1.2 Rails 文件簡介
2.2 REST 架構(gòu)
2.3 深入路由(routes)
第三章 Rails 中的視圖
6.3 異步任務(wù)及郵件發(fā)送
第二章 Rails 中的資源
3.3 視圖中的 AJAX 交互

4.3 模型中的關(guān)聯(lián)關(guān)系(Relations)

概要:

本課時講解 Rails 中 Model 和 Model 間的關(guān)聯(lián)關(guān)系。

知識點:

  1. belongs_to
  2. has_one
  3. has_many
  4. has_and_belongs_to_many
  5. self join

正文

導(dǎo)讀

如果你對一對一關(guān)系,一對多關(guān)系,多對多關(guān)系并不十分了解的話,或者你對關(guān)系型數(shù)據(jù)庫并不十分了解的話,建議你在閱讀下面的內(nèi)容前,先熟悉一下相關(guān)內(nèi)容。因為我并不想照本宣科的講解手冊。我想講的,是對它的理解,并且把我們的精力,放到設(shè)計我們的商城中。

本章涉及的知識,可以查看 Active Record Associations,或者 ActiveRecord::Associations::ClassMethods

接下來的內(nèi)容,希望能幫助你理解模型間的關(guān)聯(lián)關(guān)系。

4.3.1 模型間的關(guān)系

在前面的章節(jié)里,我們?yōu)樯坛窃O(shè)計了界面,并且使用了3個 model:

  1. User,網(wǎng)站用戶,使用 devise 提供了用戶注冊,登錄功能。
  2. Product,商品
  3. Variant,商品類型

我們在前面講解的過程中,已經(jīng)提到了 Product 和 Variant 的關(guān)系。一個 Product 有多個 Variant?,F(xiàn)在我們需要增加幾個模型,模型是根據(jù)功能來的,我們的網(wǎng)店要增加哪些功能呢?

  • 當用戶購買實物商品的時候,我們是要輸入它的收貨地址(Address)。
  • 當用戶選擇商品的時候,選擇不同的顏色和大小,會有不同的價格(Variant)。
  • 我們點擊購買,會創(chuàng)建一個購物訂單(Order),上面有我們選擇的商品,應(yīng)支付的金額,和訂單的狀態(tài)。
  • 查看用戶購買的商品類型

在我們的網(wǎng)店里,一個 User 有一個地址,每次購物的時候,會讀取這個地址作為送貨地址。

一個 Product 有多個 Variant,每個 Variant 保存它的顏色,大小等屬性。

一個用戶會有多個訂單 Order,每個訂單會顯示購買的商品 Product,以及多條購買記錄,每條記錄顯示購買的 Variant 的每個數(shù)量和應(yīng)付的價格,這里我們使用 LineItem 表示訂單的訂單項。

4.3.2 外鍵

兩個 model 之間,通過外鍵進行關(guān)聯(lián),Rails 中默認的外鍵名稱是所屬 model 的 名稱_id,比如,User 有一條 Address 記錄,那么 addresses 表上,需要增加一個數(shù)字類型的字段 user_id。而 User 的主鍵通常為 id 字段。有一些遺留的數(shù)據(jù)庫,使用的外鍵可能不是按照 Rails 默認的格式,所以在聲明外鍵關(guān)聯(lián)時,需要指定 foreign_key。

在我們創(chuàng)建 Model 的時候,可以在 generate 命令上增加外鍵關(guān)聯(lián),我們現(xiàn)在創(chuàng)建 Address 這個 Model

rails g model address user:references state city address address2 zipcode receiver phone 

在創(chuàng)建的 migration 文件中:

create_table :addresses do |t|
  t.references :user, index: true, foreign_key: true

自動增加了外鍵關(guān)聯(lián),并且將 user_id 加入索引。如果是更改其他數(shù)據(jù)庫,需要在 migration 文件內(nèi)單獨設(shè)置索引:

add_index "addresses", ["user_id"], name: "index_addresses_on_user_id"

模型間的關(guān)系,都是通過外鍵實現(xiàn)的,下面我們詳細介紹模型間的關(guān)系,并且實現(xiàn)我們商城的 Model。

4.3.3 一對一關(guān)系

一對一關(guān)系的設(shè)定,再一次體現(xiàn)了 Rails 在開發(fā)中的便捷:

class User < ActiveRecord::Base
  has_one :address
end

class Address < ActiveRecord::Base
  belongs_to :user
end

在一對一關(guān)系中,belongs_to :user 中,:user 是單數(shù),has_one :address 中,:address 也是單數(shù)。

我們進入到 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( ... )

這種方法會產(chǎn)生兩句 SQL,先是 insert 一個 address 到數(shù)據(jù)庫,然后更新它的 user_id 為剛才的 user。我們可以換一個方法:

user.address = Address.new( ... )

它只產(chǎn)生一條 insert SQL,并且會帶上 user_id 的值。

在創(chuàng)建關(guān)聯(lián)關(guān)系時,還有這樣的方法:

user.create_address( ... )
user.build_address( ... )

build_xxx 相當于 Address.new。create_xxx也會產(chǎn)生兩條 SQL,每條 SQL 都包含在一個 transaction 中。

所以我們得出結(jié)論:

把一個未保存的實例,賦值給一對一關(guān)系時,它會自動保存,并且只有一條 sql 產(chǎn)生。

先 create 一個實例,再把賦值給一對一關(guān)系時,是先保存,再更新,產(chǎn)生兩條 sql。

4.3.3.2 保存子資源

當我們編寫表單的時候,一個表單針對的是一個資源。當這個資源擁有(has_one 或 has_many)子資源時,我們可以在提交表單的時候,將它擁有的資源也保存到數(shù)據(jù)庫中。

這時,我們需要在 User中,做一個聲明:

class User < ActiveRecord::Base
  has_one :address
  accepts_nested_attributes_for :address
end

accepts_nested_attributes_for 會為 User 增加一個新的方法 address_attributes=(attributes),這樣,在創(chuàng)建 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 的參數(shù),就可以把關(guān)聯(lián)的 address 一并保存到數(shù)據(jù)庫中了。

更新記錄的時候,也可以使用同樣的方法:

user_hash = { email: "changed@123.com", address_attributes: { receiver: "Other One" } }
user.update(user_hash)

但是,這里要注意,上面的方法會把之前舊記錄的 user_id 設(shè)為 nil,然后插入一條新的記錄。這并不能真正起到更新的作用,除非所有屬性都重新復(fù)制,不然,新的 address 記錄只有 receiver 這個值。

我們在 accepts_nested_attributes_for 后增加一個參數(shù):

accepts_nested_attributes_for :address, update_only: true

這樣,update 時候會更新已有的記錄。

如果我們不能增加 update_only 屬性,為了避免創(chuàng)建無用的記錄,需要在 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 中有其對應(yīng)的方法:

<%= 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 %>

打開 代碼,在編輯一個用戶的時候,我為它增加了一個 f.fields_for 的子表單,對應(yīng)了子資源的屬性。

我想,這段代碼這并不難理解,不過我們用了 Devise 這個 gem,還需要做一點額外的處理。

打開 application_controller.rb,我們需要讓 devise 支持傳進來新增的參數(shù):

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

在我們注冊賬號的時候,并沒有創(chuàng)建 address ,但是在編輯的時候,因為它是 nil,所以不會顯示這個子表單,所以我們需要在編輯的時候創(chuàng)建一個空的 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 刪除關(guān)聯(lián)的子資源

在上一節(jié)里,我們介紹了 delete 和 destroy 方法,我們可以使用這兩個方法把關(guān)聯(lián)的 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

兩者的區(qū)別在上一節(jié)介紹過,我們注意到,delete 直接發(fā)送數(shù)據(jù)庫刪除命令,而 destroy 會將刪除命令放置到一個 sql 的事物中,因為它會觸發(fā)模型中的回調(diào),如果回調(diào)拋出異常,刪除動作會失敗。

4.3.3.5 刪除自身同時刪除關(guān)聯(lián)的子資源

在刪除某個資源的時候,我們想把它擁有的資源一并刪除,這時,我們需要給 has_one 方法,增加一個參數(shù):

has_one :address, dependent: :destroy

dependent 可以接收五個參數(shù):

參數(shù) 含義
:destroy 刪除擁有的資源
:delete 直接發(fā)送刪除命令,不會執(zhí)行回調(diào)
:nullify 將擁有的資源外鍵設(shè)為 null
:restrict_with_exception 如果擁有資源,會拋出異常,也就是說,當它 has_one 為 nil 的時候,才能正常刪除它自己
:restrict_with_error 如有擁有資源,會增加一個 errors 信息。

在 belongs_to 上,也可以設(shè)置 dependent,但它只有兩個參數(shù):

參數(shù) 含義
:destroy 刪除它所屬的資源
:delete 刪除它所屬的資源,直接發(fā)送刪除命令,不會執(zhí)行回調(diào)

兩種設(shè)定,出發(fā)角度是不同的,不過,刪除本身的同時刪除上層資源是比較危險的,需謹慎。

4.3.3.6 失去關(guān)聯(lián)關(guān)系的子資源

如果在 has_one 中設(shè)置了 dependent: :destroydependent: :delete,當子資源失去該關(guān)聯(lián)關(guān)系時,它也會被刪除。

user.address = nil

如果不設(shè)置,一個子資源失去關(guān)系時,外鍵設(shè)置為 null。

4.3.3.7 子資源維護

當一個子資源失去關(guān)聯(lián)關(guān)系,和它在關(guān)聯(lián)關(guān)系中被刪除,是一樣的。我們在設(shè)計時,應(yīng)盡量避免產(chǎn)生孤立的記錄,這些記錄外鍵為 null,或者所屬的資源已經(jīng)被刪除,他們是無意義的存在。

4.3.4 一對多關(guān)系

在電商系統(tǒng)里,一個用戶是有多個訂單(Order)的,User 中使用的是 has_many 方法:

class User < ActiveRecord::Base
  has_many :orders
end

除了名稱變?yōu)閺?fù)數(shù)形式,返回的結(jié)果是數(shù)組,其他情形和“一對一”是一樣的。

我們使用 generate 創(chuàng)建 Order:

rails g model order user:references number payment_state shipment_state

number 是訂單的唯一編號,payment_state 是付款狀態(tài),shipment_state 是發(fā)貨狀態(tài)。

payment_state 的狀態(tài)順序是:pending(等待支付),paid(已支付)。

shipment_state 的狀態(tài)順序是:pending(等待發(fā)貨),shipped(已發(fā)貨)。

這兩種狀態(tài),我們只做簡單的設(shè)計,實際中要復(fù)雜得多。

開源電商程序 spree 是一套很好的在線交易程序,因為其開源,其中的概念和定義對開發(fā)電商程序有很好的啟發(fā)。它的源代碼在 這里,目前是最新版本是 3.0.2.beta。

4.3.4.1 添加子資源

一對多關(guān)系返回的,是 CollectionProxy 實例。

當添加一對多關(guān)系時,可以很“形象”的使用:

product.variants << Variant.new
product.variants << [Variant.new, Variant.new]

執(zhí)行 << 的時候,variant 的 product_id 會自動保存為 product.id。

如果 variant 是一個未保存到數(shù)據(jù)庫的實例,<< 執(zhí)行的時候會自動將它保存,并且賦予它 product_id 值。這是一步完成的,只有一條 SQL。

但是,如果是下面的情形:

product.variants << Variant.create

會把 variant 先保存到數(shù)據(jù)庫,然后再更新它的 product_id 字段,這會產(chǎn)生兩條 SQL。

這里也可以使用 build 方法,和上面“一對一關(guān)系”不同的是,它需要在 collection 上執(zhí)行:

variant = product.variants.build( ... )
variant.save

build 返回的是一個未保存的實例。查看 product.variants,會看到它包含了一個未保存的 variant(ID 為 nil)。

另一種情形:

product.variants.build( ... )
product.save

當這個 product.save 的時候,這個 variant 也會保存到數(shù)據(jù)庫中。

4.3.4.2 刪除子資源

刪除資源的時候,可以使用幾個方法:

product.variants.delete(...)
product.variants.destroy(...)
product.variants.clear

delete 不會真正刪除掉資源,而是把它的外鍵(product_id)設(shè)為 nil,而 destroy 會真正的刪除掉它并出發(fā)回調(diào)。

他們都可以傳遞進一個實例,或者實例的集合,而并不管這個實例是否真的屬于它。

product.variants.delete(Variant.find(1))
product.variants.delete(Variant.find(1,2,3))

這樣是不是太霸道了?所以,建議用第三個方法更穩(wěn)妥些。clear 方法會把外鍵置為 nil。

如果再 has_many 上聲明了 dependent: :destroy,會用 destroy 方式把它們刪除(有回調(diào))。如果聲明的是 dependent: :delete_all,會用 delete 方法(跳過回調(diào))。這和一對一中描述是一致的。

注意:

has_many 和 has_one 上的 dependent 選項,適用以下兩種情形:

  • 刪除自身時,如何處理子資源
  • 當子資源失去該關(guān)聯(lián)關(guān)系時,如何處理該子資源

我們來看下一節(jié)。

4.3.4.3 更改子資源

當改動關(guān)系的時候,可以直接使用 =,假設(shè)我們有 ID 為 1,2,3,4 的 Variant:

product.variants = Variant.find(1,2)

這時會自動把 ID:1,ID:2 的 product_id 外鍵設(shè)為 null。

再次選擇 ID:3,ID:4 的 variant:

product.variants = Variant.find(3,4)

會自動把 ID:3,ID:4 的 product_id 外鍵設(shè)置為 product.id。

如果在 has_many 設(shè)置了 dependent: :destroy,當 UD:1 和 ID:2 失去關(guān)聯(lián)的時候,會把它們從數(shù)據(jù)庫中刪除掉。這與 has_one 中的 dependent 選項是一樣的。詳見本章前面 4.3.3.4 刪除自身同時刪除關(guān)聯(lián)的子資源。

4.3.4.4 counter_cache

“一對多”關(guān)系中,belongs_to 方法可以增加 counter_cache 屬性:

class Order < ActiveRecord::Base
  belongs_to :user, counter_cache: true
end

這時,我們需要給 users 表增加一個字段:orders_count,當我們把一個 order 保存到一對多的關(guān)系中時,orders_count 會自動 +1,當把一個資源從關(guān)系中刪除,該字段會 -1。如此我們不必去增加計算一個 user 有多少個 orders,只需要讀該字段就可以了。

向 Users 表添加 orders_count 字段:

rails g migration add_orders_count_to_users orders_count:integer 

4.3.4.5 多態(tài)

當一個資源可能屬于多種資源時,可以用到多態(tài)。舉個栗子:

商品可以評論,文章可以評論,而評論 model 對任何一個資源都是一樣的功能,所以,評論在 belongs_to 的后面,增加:

class Comment < ActiveRecord::Base
    belongs_to :commentable, polymorphic: true
end

Comment 的遷移文件,也相應(yīng)的增加設(shè)定:

t.references :commentable, polymorphic: true, index: true

如果是手動添加字段,需要這樣來寫:

t.string :commentable_type
t.integer :commentable_id

說明,查找一個多態(tài)資源時,是根據(jù)擁有者的類型(type,一般是它的類名稱)和 ID 進行匹配的。

擁有評論的 model,也需要改動下:

class Product < ActiveRecord::Base
  has_many :commentable, as: :commentable
end

class Topic < ActiveRecord::Base
  has_many :commentable, as: :commentable
end

多態(tài)并不局限于一對多關(guān)系,一對一也同樣適用。

4.3.5 中間模型和中間表

has_one 和 has_many,是兩個 model 間的操作。我們可以增加一個中間模型,描述之前兩個 model間的關(guān)系。

4.3.5.1 中間模型

我們先創(chuàng)建訂單項(LineItem)這個 model,它屬于一個訂單,也屬于一個商品類型(Variant)。

rails g model line_item order:references variant:references quantity:integer 

對于一個訂單,我們有多個訂單項,對于一個訂單項,會關(guān)聯(lián)購買的具體商品類型,那么,一個訂單擁有的商品類型,就可以通過 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 增加關(guān)聯(lián):

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 字段,因為這不符合我們的業(yè)務(wù)邏輯,所以無法查找 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 中間表

中間模型的作用,除了連接兩端模型外,更重要的是,它保存了業(yè)務(wù)中屬于中間模型的數(shù)據(jù),比如,訂單項中的 quantity 字段。如果模型不必或者沒有這種字段,可以不用增加 model,而直接使用中間表。

我們有一個功能:保存用戶購買的商品類型。這時可以使用中間表,保存購買關(guān)系。

中間表具有兩端模型的外鍵。兩端模型使用 has_and_belongs_to_many 方法(簡寫:HABTM)。

在創(chuàng)建中間表的時候,也可以使用 migration,如果在表名中包含 JoinTable 字樣,會自動創(chuàng)建中間表:

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

調(diào)整一下 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]]

利用中間表,實現(xiàn)了多對多關(guān)系。

4.3.5.3 多對多關(guān)系

查看一個用戶購買了哪些商品類型,和查看一個商品類型被哪些用戶購買,這就是多對多關(guān)系。

保存和刪除多對多關(guān)系,和一對多關(guān)系的操作是一樣的。因為我們在創(chuàng)建 migration 時,增加了索引唯一校驗,在操作時要做好異常處理,或者保存前進行判斷。

user.variants << variant
user.variants << variant
=> SQLite3::ConstraintException: columns variant_id, user_id are not unique: ...

4.3.5.4 inner join

ActiveRecord 在查詢關(guān)聯(lián)關(guān)系時,使用的是 inner join 查詢,我們可以單獨使用 join 方法,實現(xiàn)該查詢。

比如,一個簡單的 join 查詢:

% Order.joins(:line_items)
=> SELECT "orders".* FROM "orders" INNER JOIN "line_items" ON "line_items"."order_id" = "orders"."id"

也可以查詢多個關(guān)聯(lián)的:

% 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"

或者嵌套關(guān)聯(lián):

% 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"

但是,在一些更復(fù)雜的查詢中,我們需要改變 inner join 查詢?yōu)?left joinright join

User.select("users.*, orders.*").joins("LEFT JOIN `orders` ON orders.user_id = users.id")

這時返回的是全部用戶,即便它沒有訂單。這在生成一些報表時是有用的。

4.3.6 自連接

在設(shè)計模型的時候,一個模型即可以是 Catalog(類別),也可以是 Subcatalog(子類別),我們?yōu)榫W(wǎng)店添加 類別 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

這樣,我們可以實現(xiàn)分類,也可以吧商品加入到某個分類中。

4.3.7 雙向關(guān)聯(lián)

我們查找關(guān)聯(lián)關(guān)系的時候,是可以在兩邊同時查找,比如:

class User < ActiveRecord::Base
  has_one :address
end

class Address < ActiveRecord::Base
  belongs_to :user
end

我們可以 user.address,也可以 address.user,這叫做 Bi-directional,雙向關(guān)聯(lián)。(和它相反,Uni-directional,單向關(guān)聯(lián))

但是,這在我們的內(nèi)存查找中,會引起問題:

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 

兩個類并不是在內(nèi)存中指向同一個地址,他們是不同的兩個類。

為了避免這個問題,我們需要使用 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 的關(guān)聯(lián)關(guān)系上,已經(jīng)有 polymorphic,through,as 時,可以不用加 inverse_of,它自然會指向同一個 object,大家可以使用 user 和 order 之間的關(guān)聯(lián)驗證。對于 user 和 address 之間,還是應(yīng)該加上 inverse_of 選項。

4.3.8 Rspec測試

關(guān)聯(lián)關(guān)系的測試,可以使用 shoulda-matchers 這個 gem。它為 Rails 的模型間關(guān)聯(lián)提供了方便的測試方法。

比如:

RSpec.describe User, type: :model do
  it { should have_many(:orders) }
end

RSpec.describe Order, type: :model do
  it { should belong_to(:user) }
end

更多模型間關(guān)聯(lián)關(guān)系測試的方法,可以查看 ActiveRecord matchers

下一篇:6.6 常用 Gem