about 9 years ago

我們公司系統裡面有一個使用情境:有一個 method 要去查詢一個 Order,路上哪些司機車上放的貨可以送單,甚至這些司機可以要設定條件多少距離內可以送。

OK.

  • order has_many sold_products
  • sold_products belongs_to menu_item
  • driver_session has_many driver_session_items
  • driver_session_item belongs_to menu_item

這個使用情境就是要對 driver_session 造一個 scope 然後丟貨物需求數量,回傳可以繼續串接下去的 ActiveRecord::Relation。

一般 method 實作想法會是

  • 我先去問路上有哪些司機,身上有載客戶所要的貨
  • 然後對這些司機一個一個對他身上的貨夠不夠送
  • 然後列出這些司機的名單

虛擬碼會是

def my_method(order)
  results = []
  on_road_drivers.each do |driver|
    resuls << driver if driver.can_ship_this_order(order)
  end
end

然後再去下 on_roard_driver 和 can_ship_this_order 的條件。問題是這會有 db 掃描的問題。有 20 個 driver,3個 driver_session_item 就要問 20*3 = 60 次。有 10 個單要問就要問 600 次。(這是純 Ruby 解法,不是 SQL 解法)

下 SQL 難度沒有太高。不過難的是如何用 ActiveRecord 把這個作法拼出來,還能維護。

先說最後解法。

  module ClassMethods
    def fulfill(item_hash)
      item_hash.map {|item_id, amount| joins(:driver_session_items).merge(DriverSessionItem.where(:menu_item_id => item_id).where("available_qty >=?", amount).select(:driver_session_id))  }.reduce(scoped) { |scope, subquery|
        scope.where(id: subquery)
      }
    end
  end

主要想法是用 array 的 map 拼 SQL 出來再塞 subquery。是的,scope 可以用這種奇技淫巧用 map 出來再塞回去組 query。ActiveRecord 翻譯的動...

延伸閱讀可以看這兩篇:

http://stackoverflow.com/questions/6686920/activerecord-query-union
http://stackoverflow.com/questions/11838537/query-intersection-with-activerecord

不管你是要做 UNION, INTERSECTION 或者是多條件的 JOIN 都可以用這種密技解。而且 code 可以維護,不用真的寫 SQL。

← 淺談軟體專案管理,如何挑選工具與方法 做產品的一些雜談 →
 
comments powered by Disqus