iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 9
0
Modern Web

關於 Ruby on Rails,我想說的是系列 第 20

# [Day 20] 多對多關聯及多型關聯

看標題就知道今天會學到很多多多/images/emoticon/emoticon30.gif
[Day 14] 資料表關聯,以一對多為例 介紹了關聯資料表最常用的一對多,今天要更進一步從多對多開始,再深入到多型關聯,最後同時使用多對多加上多型關聯source_type特性。

多對多關聯

以7-11為例,巷口7-11有賣可樂,雪碧,舒跑,巷尾7-11有賣雪碧,舒跑,維大力。一家商店會有多個商品,一個商品也可能屬於多家商店,像這樣的關係就是多對多多對多沒辦法簡單在一個Model設置has_many,另一個設置belong_to就把這段關係表示出來,必須透過第三個資料表來紀錄這兩個Model彼此資料的關係。

建立中間資料表

已經有商店Store跟商品Product兩個Model,建立中間資料表的Model WareHouse

rails g ware_house store:references product:references

invoke  active_record
create    db/migrate/20191013123710_create_ware_houses.rb
create    app/models/ware_house.rb
invoke    test_unit
create      test/models/ware_house_test.rb
create      test/fixtures/ware_houses.yml

產生的app/models/ware_house.rb長這樣:

class WareHouse < ApplicationRecord
  belongs_to :store
  belongs_to :product
end

db/schema.rb會增加以下內容:

create_table "ware_houses", force: :cascade do |t|
  t.bigint "store_id"
  t.bigint "product_id"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["product_id"], name: "index_ware_houses_on_product_id"
  t.index ["store_id"], name: "index_ware_houses_on_store_id"
end

add_foreign_key "ware_houses", "products"
add_foreign_key "ware_houses", "stores"

使用references可以幫我們做到
1.belongs_to關聯自動建好了
2.產生add_foreign_key建立外部鍵
3.外部鍵加上index,加快查詢時的速度

當然產生Model 時參數改用store_id:interger product_id:interger也可以。這樣belongs_toadd_index就要自己動手加。

透過中間資料表,建立多對多關係

Store Model app/models/store.rb,加上以下兩行:

class Store < ApplicationRecord
  has_many :ware_houses
  has_many :products, through: :ware_houses
end

然後同樣也在app/models/product.rb加上這兩行:

class Product < ApplicationRecord
  has_many :ware_houses
  has_many :stores, through: :ware_houses
end

WareHouse Model 同時 belongs_to Store 以及 Product 這兩個 Model,然後 Store 跟 Product 這兩個 Model 也都 has_many WareHouse

through參數可以讓Rails知道要透過哪個資料表去找到多對多關聯。

source

搭配through使用,想給關聯另外取名字時,需要加上source指名是哪一種物件。假如想把商品對應到的商店改稱為shop,可以這樣做

class Product < ApplicationRecord
  has_many :ware_houses
  has_many :shops, through: :ware_houses, source: :store
end

source的參數是Model,所以是store不是stores

多對多關聯如下圖:


圖片取自為你自己學Ruby

多對多的關係,從ware_house裡的每一筆資料來看,其實是一對一的關係,因為每筆資料都屬於belongs_to某個 store_id 跟某個 product_id

多對多關聯操作

先建立巷口7-11,再設定巷口7-11有賣前兩個商品

store = Store.create(title: '巷口7-11')
store.products = Product.first(2)

執行的SQL如下:

Product Load (0.3ms)  SELECT  "products".* FROM "products" ORDER BY "products"."id" DESC LIMIT $1  [["LIMIT", 2]]
Product Load (0.6ms)  SELECT "products".* FROM "products" INNER JOIN "ware_houses" ON "products"."id" = "ware_houses"."product_id" WHERE "ware_houses"."store_id" = $1  [["store_id", 1]]
  (0.1ms)  BEGIN
WareHouse Create (0.9ms)  INSERT INTO "ware_houses" ("store_id", "product_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["store_id", 1], ["product_id", 1], ["created_at", "2019-10-13 13:15:12.991751"], ["updated_at", "2019-10-13 13:15:12.991751"]]
WareHouse Create (0.4ms)  INSERT INTO "ware_houses" ("store_id", "product_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["store_id", 1], ["product_id", 2], ["created_at", "2019-10-13 13:15:12.994419"], ["updated_at", "2019-10-13 13:15:12.994419"]]
 (40.5ms)  COMMIT

從SQL語言INSERT INTO可以看到真正建立的資料是在ware_houses資料表,ware_houses會把store_idproduct_id記錄下來。

查詢某商品在哪些商店有賣時,給定了一個product_id,然後ware_house會去找身上所有符合這個product_idstore_id,再把所有的store列出來:

Product.first.stores

Product Load (0.4ms)  SELECT  "products".* FROM "products" ORDER BY "products"."id" ASC LIMIT $1  [["LIMIT", 1]]
Store Load (0.4ms)  SELECT  "stores".* FROM "stores" INNER JOIN "ware_houses" ON "stores"."id" = "ware_houses"."store_id" WHERE "ware_houses"."product_id" = $1 LIMIT $2  [["product_id", 1], ["LIMIT", 11]]
# => #<ActiveRecord::Associations::CollectionProxy [#<Store id: 1, title: "巷口7-11", address: nil, tel: nil, user_id: nil, created_at: "2019-10-13 13:11:01", updated_at: "2019-10-13 13:11:01">, #<Store id: 3, title: "巷尾7-11", address: nil, tel: nil, user_id: nil, created_at: "2019-10-13 13:18:56", updated_at: "2019-10-13 13:18:56">]> 

有趣的是可以看到,最後是透過FROM "stores" INNER JOIN "ware_houses" ON "stores"."id" = "ware_houses"."store_id"找兩個資料表的交集來查詢。

多對多關聯就介紹到這,再來要多型關聯

多型關聯

每個顧客可以有很多張圖片,每個商品也可以有很多張圖片,這時圖片 Model 就屬於顧客&商品2個Model,但圖片Model也不像前面的WareHouse作為中間資料表,這時就要把圖片Model想成擁有公開介面,提供給其他Model來取用,也就是OO的多型概念。

class Image < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end
 
class Customer <ApplicationRecord
  has_many :images, as: :imageable     #透過as 來串接
end
 
class Product < ApplicationRecord
  has_many :images, as: :imageable
end

Image提供imageable介面給Customer Model 跟Product Model 做關聯。
雖然Image belongs_to :imageable但是實際上沒有imageable這個資料表,多型關聯,要加上polymorphic: true參數。
所有要取用imageable這個公開介面的model(ex.Product),除了用has_many :images建立關聯,也要透過as參數來指定介面為imageable

分別建立商品跟顧客的圖片:

product = Product.create(title: '七代目火影', price: 999)
product.images.create(name: '日向雛田')
# => #<Image id: 1, name: "日向雛田", imageable_id: 4, imageable_type: "Product", created_at: "2019-10-13 14:21:23", updated_at: "2019-10-13 14:21:23"> 

customer = Customer.create(name: '板木老大')
customer.images.create(name: '貓老大')
# => #<Image id: 2, name: "貓老大", imageable_id: 1, imageable_type: "Customer", created_at: "2019-10-13 14:25:32", updated_at: "2019-10-13 14:25:32"> 

使用imageable反查

Image Model 的實體使用 @image.imageable 看擁有這張圖片的是誰(父物件)。但首先需要先在遷移裡,加入外鍵 foreign_id(*_id)與類型(*_type)欄位。字串的_type欄位說明是哪一種Model。

class CreateImages < ActiveRecord::Migration[5.2]
  def change
    create_table :images do |t|
      t.string :name
      t.integer :imageable_id
      t.string :imageable_type

      t.timestamps
    end
  end
end

反查最後一張圖片是屬於哪個商品還是哪個顧客

Image.last.imageable
# => #<Customer id: 1, name: "板木老大", phone: nil, created_at: "2019-10-13 14:24:41", updated_at: "2019-10-13 14:24:41"> 

借用Rails Guides的圖:


圖中的Employee跟Picture就是剛才的Customer跟Image的角色。Picture 由身上的imageable_type來決定要去哪個Model找資料,再由imageable_id找到對應的資料。


我們剛才討論的是一對多關係多型關聯,一個商品有很多張圖,一張圖只屬於某個顧客或商品。

一張圖片想給不只一個商品使用呢?多對多的關聯資料表該怎麼建立?

答案是使用下面要介紹的多對多介面

多對多的多型介面

使用StackOver Flow 上對於多對多介面參數 source_type的討論來介紹。我希望:
1.每一本書有多個tag
2.每一部電影也有多個tag
3.一個tag對應到多本書,或一個tag對應到多部電影

我需要source_type參數來做多型介面:

class Tag < ActiveRecord::Base
  has_many :taggings, :dependent => :destroy
  has_many :books, :through => :taggings, :source => :taggable, :source_type => "Book"
  has_many :movies, :through => :taggings, :source => :taggable, :source_type => "Movie"
end

class Tagging < ActiveRecord::Base
  belongs_to :taggable, :polymorphic => true
  belongs_to :tag
end

class Book < ActiveRecord::Base
  has_many :taggings, :as => :taggable
  has_many :tags, :through => :taggings
end

class Movie < ActiveRecord::Base
  has_many :taggings, :as => :taggable
  has_many :tags, :through => :taggings
end
  • taggings作為Book vs Tag 還有Movie vs Tag多對多關聯的中間資料表
  • Tagging也提供taggable介面讓BookMovie可以跟Tagging建立關聯。
  • Tag Model要跟其他三個Model建立多對多關聯,在跟Movie還有Book建立has_many關聯時,要指定source_type是哪個Model

Model 關係圖:

當我想要找所有tag是Fun的書,可以這樣做Query:

tag = tag.find_by_name('Fun')
tag.books

如果沒有指定source_type,我只能找出一堆name是Fun的Tag,卻不能區分books跟movies。


總結:
多對多多型關聯平常幾乎用不到,我也是上週工作時剛好遇到這個複雜的資料表,看了良久才覺得自己懂了,真是錯綜復雜啊。經過今天的介紹,希望各位看倌的關聯資料表實力都更上一層。


上一篇
[Day 19] 使用 ActiveJob & Sidekiq 背景執行工作
下一篇
[Day 21] 交易 transaction
系列文
關於 Ruby on Rails,我想說的是23
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言