看標題就知道今天會學到很多多多
[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_to
,add_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知道要透過哪個資料表去找到多對多關聯。搭配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_id
跟product_id
記錄下來。
查詢某商品在哪些商店有賣時,給定了一個product_id
,然後ware_house
會去找身上所有符合這個product_id
的store_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">
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
介面讓Book
跟Movie
可以跟Tagging
建立關聯。Movie
還有Book
建立has_many
關聯時,要指定source_type
是哪個Model
當我想要找所有tag是Fun
的書,可以這樣做Query:
tag = tag.find_by_name('Fun')
tag.books
如果沒有指定source_type
,我只能找出一堆name是Fun
的Tag,卻不能區分books跟movies。
總結:
多對多多型關聯平常幾乎用不到,我也是上週工作時剛好遇到這個複雜的資料表,看了良久才覺得自己懂了,真是錯綜復雜啊。經過今天的介紹,希望各位看倌的關聯資料表實力都更上一層。