about 3 years ago

Rails 的 ORM 提供很便利的語法讓工程師可以輕鬆地對資料庫作查詢,但是在某些場景裡仍讓工程師們感到就有些美中不足,例如: 聯集(UNION)

假定系統需要一個 my tracking list 的頁面,這個頁面必需列出 manager 和 tracking product,並且針對它們去做更進一步的查詢操作,如列出 publish 的 product,你會怎麼設計?

似乎這一切聽起來令人頭疼,但看完後面的範例,你也能暸解實現聯集的作法。

product.rb
#  name         :string(255)

#  description  :text

#  status       :string(255)

#  manager_id   :integer

 
class Product < ActiveRecord::Base 
  belongs_to :manager, :foreign_key => :manager_id, :class_name => "User"
  
  has_many :tracking_lists
  has_many :tracking_users, :through => :tracking_lists, :source => :user  
end
user.rb
 
class User < ActiveRecord::Base
  has_many :products 
  
  has_many :tracking_lists
  has_many :tracking_products, :through => :tracking_lists, :source => :product
end
track_list.rb
#  id           :integer

#  user_id      :integer

#  product_id   :integer

 
class TrackList < ActiveRecord::Base
  belongs_to :user
  belongs_to :product
end
起初我們會想到 Array 的簡單作法
@products = Set.new
manage_products = @user.products.is_publish
tracking_products = @user.tracking_products.is_publish

@products.merge(manage_products)
@products.merge(tracking_products)

維護性: 低

  • (優) 寫起來快,幾乎不用思考就寫完了...
  • (劣) 需要下兩道 query,且無法合併一起查詢
  • (劣) Arrayinstance 無法做更進一步的查詢操作,例: order(:created_at)...etc 。
  • (劣) 速度比較慢

但我們始終覺得這樣的做法效率太差,且維護性非常的低...

然後我們想到 find_by_sql + left join 的作法
@products = Product.find_by_sql(["
    SELECT p0.* FROM product as p0 
    LEFT JOIN track_lists as t1 on t1.product_id = p0.id AND t1.user_id = p0.manager_id
    where p0.manager_id = ? AND p0.status = 'published';
    ", user.id])

維護性: 中

  • (優) 只有一道 query
  • (劣) 需以 product 的 query 條件為主,當 user 沒有被 assigned manage 某樣商品時,結果就會為空
  • (劣) 由於 find_by_sql 出來的結果是 Array,無法做更進一步的查詢操作,例: order(:created_at)...etc 。

然而這種解法還是不夠方便,且當有不同的 query 需求,例如: is_draft?,我們就必須再重寫一次 method。

最後我們發現可以這樣做 from + union
manage_products_sql = @user.products.to_sql
tracking_products_sql = @user.tracking_products.to_sql

@products = Product.from("( #{ manage_products_sql } UNION #{ tracking_products_sql } ) AS products").is_publish

維護性: 高

  • (優) 只有一道 query
  • (優) from 出來的結果是 ActiveRecord::Relation,我們可以續用 QueryMethods 做更進一步的查詢操作
  • (優) 簡單好維護

備註:

  • to_sql 只會產生 query 的字串,並不會實際去下 query
  • UNION 是 mysql 的聯集語法,且預設的 distinct 只會取一次相同的資料

參考資料 ActiveRecord::QueryMethods

← 如何開始一個簡單的 RSpec 測試?
 
comments powered by Disqus