ransack
是一個基於Model
層級的Gem
,使用Ransack
,可以將原本的Sql Statement
寫得更簡潔。Ransack
為Rails
數一數二好用的Gem
,也因此有許多人幫忙為ransack
背書。
Ransack
會依據傳過來的參數,自動SubQuery
、Join
、Preload
,因此複雜的問題我們可以使用Ransack
語句來處理,更複雜的問題,或者enum
的查詢,可以交給自定義的Ransacker
,在Ransacker
裡頭使用ransack
自定義搜尋,做成更複雜的搜尋系統。
使用ransack
,就像是使用sql statement
一樣,我們只要知道會遇到哪些雷,知道哪些雷不要踩後,就可以很舒服的使用。
查找資料最需要用實例來解釋,以下列出幾種曾經幾個使用過的ransack
語句。
_cont
為模糊搜尋的意思Product.ransack(title_zh_cont: '釉彩').result
SELECT `products`.* FROM `products` WHERE `products`.`title_zh` LIKE '%釉彩%' LIMIT 11
搜尋子訂單內的訂單項目對應的商品名稱
variant
,variant
又關聯product
,而我們要找product
的title_zh
欄位,因此寫成variant_product_title_zh_cont
ransack
自動幫我們用left_join
起來sub_order.order_items.ransack(variant_product_title_zh_cont: '珍珠').result
SELECT `order_items`.* FROM `order_items` LEFT OUTER JOIN `variants` ON `variants`.`id` = `order_items`.`variant_id` LEFT OUTER JOIN `products` ON `products`.`id` = `variants`.`product_id` WHERE `order_items`.`sub_order_id` = 1 AND `products`.`title_zh` LIKE '%珍珠%' LIMIT 11
找尋一個禮拜前創建的訂單、不能有任一張退貨單為申請中、不能為全部的退貨單狀態為「審查不同意」
_does_not_match_any
:沒有任何一個符合_does_not_match_all
:沒有全部都符合lt
/gt
/lteq
/ gteq
:小於、大於、小於等於、大於等於Order.instance_eval do
# 排除尚在審查的退貨單的訂單
scope :return_order_reviewed, -> { ransack(return_orders_status_does_not_match_any: :applied).result }
# 一個禮拜前
scope :week_ago, -> { ransack(done_at_lt: Tool.appreciation_period.ago).result }
# 若該訂單底下的所有退貨單審查不通過,則不用同步pos
scope :not_all_return_failed, -> { ransack(return_orders_status_does_not_match_all: :return_failed).result }
end
Order.return_order_reviewed.week_ago.not_all_return_failed
SELECT `orders`.* FROM `orders` WHERE `orders`.`id` NOT IN (SELECT `return_orders`.`order_id` FROM `return_orders` WHERE `return_orders`.`order_id` = `orders`.`id` AND NOT ((`return_orders`.`status` NOT LIKE 0))) AND `orders`.`done_at` < '2021-09-27 14:54:23' AND `orders`.`id` NOT IN (SELECT `return_orders`.`order_id` FROM `return_orders` WHERE `return_orders`.`order_id` = `orders`.`id` AND NOT ((`return_orders`.`status` NOT LIKE 4))) LIMIT 11
以下為判斷最新購物車內物品是否為空,以及車裡面的項目是否在1天內更新過的語法。由SQL Statement
可以知道,兩者的意思相同,cart_items_not_null: true
是多餘的,理由也很好理解,因為沒有購物車項目就不會找到1天內更新過的購物車項目
_null
:空值_not_null
:非空值Cart.ransack(cart_items_updated_at_lteq: 1.days.ago, cart_items_not_null: true).result.to_sql
#=> "SELECT `carts`.* FROM `carts` LEFT OUTER JOIN `cart_items` ON `cart_items`.`cart_id` = `carts`.`id` WHERE `cart_items`.`updated_at` <= '2021-09-08 06:10:04.641275'"
Cart.ransack(cart_items_updated_at_lteq: 1.days.ago).result.to_sql
#=> "SELECT `carts`.* FROM `carts` LEFT OUTER JOIN `cart_items` ON `cart_items`.`cart_id` = `carts`.`id` WHERE `cart_items`.`updated_at` <= '2021-09-08 06:10:14.869951'"
另外關於這台車,還有值得討論的地方。
以下轉換為Sql Statement
時,使用的是LEFT (OUTER) JOIN
語法 ,上一回提過搜尋的結果會造成找出的結果有重複的筆數,因此我們需要用distinct
語法來濾除。當時購物車沒有使用distinct
而導致對某使用者重複推播,所以回過頭還是要注意Sql Statement
是否會有找到重複資料的問題
Cart.ransack(cart_items_updated_at_gt: 1.days.ago, customer_id_null: false).result.count
#=> (1.1ms) SELECT COUNT(*) FROM `carts` LEFT OUTER JOIN `cart_items` ON `cart_items`.`cart_id` = `carts`.`id` WHERE (`cart_items`.`updated_at` > '2021-09-23 23:29:34.891970' AND `carts`.`customer_id` IS NOT NULL)
#=> 10
Cart.ransack(cart_items_updated_at_gt: 1.days.ago, customer_id_null: false).result.uniq.count
#=> Cart Load (2.7ms) SELECT `carts`.* FROM `carts` LEFT OUTER JOIN `cart_items` ON `cart_items`.`cart_id` = `carts`.`id` WHERE (`cart_items`.`updated_at` > '2021-09-23 23:29:42.348931' AND `carts`.`customer_id` IS NOT NULL)
#=> 2
#============== 使用distinct
Cart.ransack(cart_items_updated_at_gt: 1.days.ago, customer_id_null: false).result.distinct.count
#=> (4.6ms) SELECT COUNT(DISTINCT `carts`.`id`) FROM `carts` LEFT OUTER JOIN `cart_items` ON `cart_items`.`cart_id` = `carts`.`id` WHERE (`cart_items`.`updated_at` > '2021-09-23 23:38:08.748560' AND `carts`.`customer_id` IS NOT NULL)
#=> 2
⭐️ ⭐️ ⭐️ ⭐️ Ransack
所需要知道的雷,只會發生於1對多has_many
,當被left_join
, join
時可能會出現重複筆以外,其他用法基本上都毋需太過擔心。
⭐️ 搜尋條件有開始、結束時間、關鍵字、下拉式
SELECT `blogs`.* FROM `blogs` WHERE ((`blogs`.`title` LIKE '%標題%' OR `blogs`.`content` LIKE '%標題%') AND `blogs`.`created_at` >= '2020-09-26 16:00:00' ANlogs`.`created_at` < '2021-09-27 16:00:00') LIMIT 10 OFFSET 0
⭐️ 搜尋條件只有開始和結束時間和關鍵字
SELECT `blogs`.* FROM `blogs` WHERE ((`blogs`.`title` LIKE '%標題%' OR `blogs`.`content` LIKE '%標題%') AND `blogs`.`created_at` >= '2020-09-26 16:00:00' ANlogs`.`created_at` < '2021-09-27 16:00:00') LIMIT 10 OFFSET 0
⭐️ 搜尋條件只有開始和結束時間
SELECT `blogs`.* FROM `blogs` WHERE (`blogs`.`created_at` >= '2020-09-26 16:00:00' AND `blogs`.`created_at` < '2021-09-27 16:00:00') LIMIT 10 OFFSET 0
⭐️ ⭐️ ⭐️ ⭐️ 前面提到過,只要下的關聯為belongs_to
, has_one
,都不用擔心會有查詢重複筆數的問題。
ReturnOrder.ransack(m: 'or', return_actual_rebate_amount_lteq: 0, return_actual_rebate_amount_null: true).result
class Post < ActiveRecord::Base
belongs_to :author
# Abbreviate :author_first_name_or_author_last_name to :author
ransack_alias :author, :author_first_name_or_author_last_name
end
ransack
搭配 enum
的用法
# enum
class SubOrder < ApplicationRecord
enum sex: { male: 'M', female: 'F', unavailable: 'N' }
ransacker :sex, formatter: proc { |v| sexes[v] }
# ransacker :vip_level, formatter: proc { |v| VIP_LEVEL_TRANS_MAP[v] }
end
# 使用 ransacker 以前
SubOrder.ransack(sex: sexes[:male])
# 使用 ransacker 以後
SubOrder.ransack(sex: :male)
我們也可以自定義 ransack
class SubOrder < ApplicationRecord
scope :unshipped, -> (tab) do
if tab.in? [:waiting, 'waiting']
ransack(status_eq: Status::PROCESSING, shipping_status_eq: ShippingStatus::READY).result
elsif tab.in? [:failed, 'failed']
ransack(status_not_eq: Status::DONE, shipping_status_eq: ShippingStatus::FAILED).result
else
SubOrder.all
end
end
def self.ransackable_scopes(_auth_object = nil)
[:unshipped]
end
end
SubOrder.ransack(unshipped: :waiting).result.to_sql
#=> "SELECT `sub_orders`.* FROM `sub_orders` WHERE (`sub_orders`.`status` = 'processing' AND `sub_orders`.`shipping_status` = 'ready')"
class ReturnOrder < ApplicationRecord
enum status: %i(applied processing refund_failed return_failed done)
ransacker :status, formatter: proc { |v| statuses[v] }
# 待處理/退貨失敗/全部退貨單
scope :tab, -> (tab) do
if tab.in? [:all, 'all']
all
elsif tab.in? [:return_failed, 'return_failed']
where(status: :return_failed)
else
where(status: :processing)
end
end
def self.ransackable_scopes(_auth_object = nil)
[:tab]
end
end
ReturnOrder.ransack(tab: :processing).result
#=> SELECT `return_orders`.* FROM `return_orders` WHERE `return_orders`.`status` = 1 LIMIT 11
我們可以透過ransackable_attributes
,搜尋該model
可以使用的ransack
屬性。
> SubOrder.ransackable_attributes
#=> ["id", "brand_id", "order_id", "created_at", "updated_at", "number", "store_id", "shipping_status", "shipped_at", "arrived_at", "received_at", "status_manual_changed", "shipping_status_manual_changed", "failed_at", "remark", "status", "shipments_count", "pickup_personally", "admin_force_return"]
原本打算將Gem
相關的文章集結成系列介紹,但因在datatable
使用了很多次,加上我也答應我的同事要寫一篇文介紹ransack
,因此有這篇文章的誕生。