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

鍍金池/ 教程/ Ruby/ 4.3 模型中的關(guān)聯(lián)關(guān)系(Relations)
寫(xiě)在后面
寫(xiě)在前面
第六章 Rails 的配置及部署
第四章 Rails 中的模型
4.4 模型中的校驗(yàn)(Validates)
1.3 用戶界面(UI)設(shè)計(jì)
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 文件簡(jiǎn)介
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)

概要:

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

知識(shí)點(diǎn):

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

正文

導(dǎo)讀

如果你對(duì)一對(duì)一關(guān)系,一對(duì)多關(guān)系,多對(duì)多關(guān)系并不十分了解的話,或者你對(duì)關(guān)系型數(shù)據(jù)庫(kù)并不十分了解的話,建議你在閱讀下面的內(nèi)容前,先熟悉一下相關(guān)內(nèi)容。因?yàn)槲也⒉幌胝毡拘频闹v解手冊(cè)。我想講的,是對(duì)它的理解,并且把我們的精力,放到設(shè)計(jì)我們的商城中。

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

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

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

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

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

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

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

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

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

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

4.3.2 外鍵

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

在我們創(chuàng)建 Model 的時(shí)候,可以在 generate 命令上增加外鍵關(guān)聯(lián),我們現(xiàn)在創(chuàng)建 Address 這個(gè) 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

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

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

模型間的關(guān)系,都是通過(guò)外鍵實(shí)現(xiàn)的,下面我們?cè)敿?xì)介紹模型間的關(guān)系,并且實(shí)現(xiàn)我們商城的 Model。

4.3.3 一對(duì)一關(guān)系

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

class User < ActiveRecord::Base
  has_one :address
end

class Address < ActiveRecord::Base
  belongs_to :user
end

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

我們進(jìn)入到 console 里來(lái)測(cè)試一下:

user = User.first
user.address
=> nil

4.3.3.1 新建子資源

如何為 user 保存 address 呢?

一種是使用 Address 的類方法 create

Address.create(user_id: user.id, ...)

我們也可以省去 id 的寫(xiě)法,直接寫(xiě)上所屬的實(shí)例:

Address.create(user: user, ...)

一種是使用實(shí)例方法:

address = Address.new
address.user = user
address.save

或者:

user.address = Address.create( ... )

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

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

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

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

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

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

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

把一個(gè)未保存的實(shí)例,賦值給一對(duì)一關(guān)系時(shí),它會(huì)自動(dòng)保存,并且只有一條 sql 產(chǎn)生。

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

4.3.3.2 保存子資源

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

這時(shí),我們需要在 User中,做一個(gè)聲明:

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

accepts_nested_attributes_for 會(huì)為 User 增加一個(gè)新的方法 address_attributes=(attributes),這樣,在創(chuàng)建 User 的 時(shí)候:

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 的時(shí)候,傳遞入 Address 的參數(shù),就可以把關(guān)聯(lián)的 address 一并保存到數(shù)據(jù)庫(kù)中了。

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

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

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

我們?cè)?accepts_nested_attributes_for 后增加一個(gè)參數(shù):

accepts_nested_attributes_for :address, update_only: true

這樣,update 時(shí)候會(huì)更新已有的記錄。

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

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

我想,這段代碼這并不難理解,不過(guò)我們用了 Devise 這個(gè) gem,還需要做一點(diǎn)額外的處理。

打開(kāi) application_controller.rb,我們需要讓 devise 支持傳進(jìn)來(lái)新增的參數(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

在我們注冊(cè)賬號(hào)的時(shí)候,并沒(méi)有創(chuàng)建 address ,但是在編輯的時(shí)候,因?yàn)樗?nil,所以不會(huì)顯示這個(gè)子表單,所以我們需要在編輯的時(shí)候創(chuàng)建一個(gè)空的 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? %>
  ...

當(dāng)然,我們也可以在注冊(cè)的時(shí)候提供地址表單,大家不妨一試。

4.3.3.4 刪除關(guān)聯(lián)的子資源

在上一節(jié)里,我們介紹了 delete 和 destroy 方法,我們可以使用這兩個(gè)方法把關(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é)介紹過(guò),我們注意到,delete 直接發(fā)送數(shù)據(jù)庫(kù)刪除命令,而 destroy 會(huì)將刪除命令放置到一個(gè) sql 的事物中,因?yàn)樗鼤?huì)觸發(fā)模型中的回調(diào),如果回調(diào)拋出異常,刪除動(dòng)作會(huì)失敗。

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

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

has_one :address, dependent: :destroy

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

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

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

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

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

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

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

user.address = nil

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

4.3.3.7 子資源維護(hù)

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

4.3.4 一對(duì)多關(guān)系

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

class User < ActiveRecord::Base
  has_many :orders
end

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

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

rails g model order user:references number payment_state shipment_state

number 是訂單的唯一編號(hào),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),我們只做簡(jiǎn)單的設(shè)計(jì),實(shí)際中要復(fù)雜得多。

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

4.3.4.1 添加子資源

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

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

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

執(zhí)行 << 的時(shí)候,variant 的 product_id 會(huì)自動(dòng)保存為 product.id。

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

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

product.variants << Variant.create

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

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

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

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

另一種情形:

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

當(dāng)這個(gè) product.save 的時(shí)候,這個(gè) variant 也會(huì)保存到數(shù)據(jù)庫(kù)中。

4.3.4.2 刪除子資源

刪除資源的時(shí)候,可以使用幾個(gè)方法:

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

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

他們都可以傳遞進(jìn)一個(gè)實(shí)例,或者實(shí)例的集合,而并不管這個(gè)實(shí)例是否真的屬于它。

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

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

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

注意:

has_many 和 has_one 上的 dependent 選項(xiàng),適用以下兩種情形:

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

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

4.3.4.3 更改子資源

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

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

這時(shí)會(huì)自動(dòng)把 ID:1,ID:2 的 product_id 外鍵設(shè)為 null。

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

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

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

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

4.3.4.4 counter_cache

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

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

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

向 Users 表添加 orders_count 字段:

rails g migration add_orders_count_to_users orders_count:integer 

4.3.4.5 多態(tài)

當(dāng)一個(gè)資源可能屬于多種資源時(shí),可以用到多態(tài)。舉個(gè)栗子:

商品可以評(píng)論,文章可以評(píng)論,而評(píng)論 model 對(duì)任何一個(gè)資源都是一樣的功能,所以,評(píng)論在 belongs_to 的后面,增加:

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

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

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

如果是手動(dòng)添加字段,需要這樣來(lái)寫(xiě):

t.string :commentable_type
t.integer :commentable_id

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

擁有評(píng)論的 model,也需要改動(dòng)下:

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

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

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

4.3.5 中間模型和中間表

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

4.3.5.1 中間模型

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

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

對(duì)于一個(gè)訂單,我們有多個(gè)訂單項(xiàng),對(duì)于一個(gè)訂單項(xiàng),會(huì)關(guān)聯(lián)購(gòu)買的具體商品類型,那么,一個(gè)訂單擁有的商品類型,就可以通過(guò) 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

我們進(jìn)到終端里進(jìn)行查找:

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 語(yǔ)法。

LineItem 是兩個(gè)模型,Order 和 Variant 的中間模型,它表示訂單中的每一項(xiàng)。但是,中間模型不一定要使用兩個(gè) belongs_to 連接兩邊的模型,比如:

class User < ActiveRecord::Base
  has_many :orders
  has_many :line_items, through: :orders
end

進(jìn)到終端,我們查看一個(gè)用戶有哪些訂單項(xiàng):

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]]

從左邊可以查到右邊資源,那么,可以通過(guò)中間表,從右邊查找左邊資源么?

我們給 Variant 增加關(guān)聯(lián):

class Variant < ActiveRecord::Base
  belongs_to :product
  has_many :line_item
  has_many :orders, through: :line_item
end

進(jìn)入終端:

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]]

因?yàn)橹虚g表 LineItem 擁有兩邊的外鍵,所以可以查找 variant 的 orders。但是 orders 上沒(méi)有 line_item_id 字段,因?yàn)檫@不符合我們的業(yè)務(wù)邏輯,所以無(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ù),比如,訂單項(xiàng)中的 quantity 字段。如果模型不必或者沒(méi)有這種字段,可以不用增加 model,而直接使用中間表。

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

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

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

rails g migration CreateJoinTable users variants:uniq

運(yùn)行 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

在終端里測(cè)試:

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]]

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

4.3.5.3 多對(duì)多關(guān)系

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

保存和刪除多對(duì)多關(guān)系,和一對(duì)多關(guān)系的操作是一樣的。因?yàn)槲覀冊(cè)趧?chuàng)建 migration 時(shí),增加了索引唯一校驗(yàn),在操作時(shí)要做好異常處理,或者保存前進(jìn)行判斷。

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)系時(shí),使用的是 inner join 查詢,我們可以單獨(dú)使用 join 方法,實(shí)現(xiàn)該查詢。

比如,一個(gè)簡(jiǎn)單的 join 查詢:

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

也可以查詢多個(gè)關(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")

這時(shí)返回的是全部用戶,即便它沒(méi)有訂單。這在生成一些報(bào)表時(shí)是有用的。

4.3.6 自連接

在設(shè)計(jì)模型的時(shí)候,一個(gè)模型即可以是 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

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

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

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

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)存查找中,會(huì)引起問(wèn)題:

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 

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

為了避免這個(gè)問(wèn)題,我們需要使用 inverse_of:

class User < ActiveRecord::Base
  has_one :address, inverse_of: :user
end

class Address < ActiveRecord::Base
  belongs_to :user, inverse_of: :address
end

當(dāng) model 的關(guān)聯(lián)關(guān)系上,已經(jīng)有 polymorphic,through,as 時(shí),可以不用加 inverse_of,它自然會(huì)指向同一個(gè) object,大家可以使用 user 和 order 之間的關(guān)聯(lián)驗(yàn)證。對(duì)于 user 和 address 之間,還是應(yīng)該加上 inverse_of 選項(xiàng)。

4.3.8 Rspec測(cè)試

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

比如:

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)系測(cè)試的方法,可以查看 ActiveRecord matchers

下一篇:6.6 常用 Gem