前幾天在工作中遇到要根據多個國家的code查詢出對應的國家并將結果按code的順序來排序的需求,但我們在rails中做查詢時通常得到的結果都是有順序的(下面都會選擇id來做自定義排序)。
Role.where(id: [2, 1, 5]).map(&:id) #=> [1, 2, 5]
這里就和我上面所講的需求不一致了,我們希望查詢結果的順序是[2, 1, 5]。想了一下沒找到什么優雅的解決方式就在dash和某中文網站(可能是這個需求用中文不太好描述。。)搜索沒結果后,轉而在google上搜了下,挑了靠前的五到六個網頁看了下后,發現方法基本都是那幾種,這里就做一個小整合。
一.case...when相關方法或同類原理
這是看到的最多的方法,有很多變種。這里先做一個記錄。
- case...when的思路原型是這樣:
case :b
when :a then 1
when :b then 2
when :c then 3
end
#=> 2
這里第一種是應用sql語句的一種寫法和order by連用,在查詢完后做不規則排序。sql語句原型是
SELECT * FROM roles WHERE id IN (1, 2, 5)
ORDER BY CASE id
WHEN 2 THEN 0
WHEN 1 THEN 1
WHEN 5 THEN 2
ELSE 3 END;
#=> 2, 1, 5
下面的方法就是將該段sql語句用ruby表示出來
def find_ordered(ids)
order_clause = "CASE id "
ids.each_with_index do |id, index|
order_clause << sanitize_sql_array(["WHEN ? THEN ? ", id, index])
end
order_clause << sanitize_sql_array(["ELSE ? END", ids.length])
where(id: ids).order(order_clause)
end
Role.find_ordered([2, 1, 5]).map(&:id)
#=> [2, 1, 5]
- 第二種是同樣的思想,先查詢然后將查詢結果的id與要求的id順序做比較。
ids = [2, 1, 5]
records = Role.find(ids)
result = ids.collect {|id| records.detect {|x| x.id == id}}.map(&:id)
#=> [2, 1, 5]
也可以將id存為key,所在的那條記錄存為value(這個也有多種方法,下面寫較簡單的兩種),再進行比較。
Role.find(ids).index_by(&:id).slice(*ids).values.map(&:id)
#=> [2, 1, 5]
ids = [2, 1, 5]
records = Role.find(ids).group_by(&:id)
result = ids.map {|id| records[id].first}.map(&:id)
#=> [2, 1, 5]
二.mysql的特殊方法
- mysql有個特性是可以按字段排序
ORDER BY FIELD
具體語法是:
SELECT id FROM roles WHERE id IN (1, 2, 5)
ORDER BY FIELD(id, 2, 1, 5);
#=> 2, 1, 5
用rails轉化后就是
Role.where(id: ids).order("FIELD(id, #{ids.join(',')})").map(&:id)
#=> [2, 1, 5]
三.postgresql的特殊方法
- 那在pg中就無法使用mysql的field特性了,但是pg也有自己的方式來自定義排序。
position(substring in string)可以返回指定子字符串的位置
eg.position('om' in 'Thomas') #=> 3
ids = [2, 1, 5]
Role.where(id: ids).order("position(id::text in '#{ids.join(',')}')").map(&:id)
#=> [2, 1, 5]
ps: 通過這樣的方式做自定義排序也存在問題,如果ids = [12, 2, 1, 5]
, 那么結果就會出現
User.where(id: ids).order("position(id::text in '#{ids.join(',')}')").map(&:id)
#=> [1, 12, 2, 5]
- 在postgresql 9.4版本開始,我們可以利用WITH ORDINALITY來設置返回值。那這里我們配合unnest(將一個數組擴展為多行記錄)和JOIN使用,可以通過先新建行記錄確定排序順序然后通過表連接查詢出對應順序的記錄
ids = [12, 2, 1, 5]
User.joins("JOIN unnest(array#{ids}) WITH ORDINALITY t(id, ord) USING (id) ORDER BY t.ord").map(&:id)
#=> [12, 2, 1, 5]
四.gem包:order_as_specified
- 通過order_as_specified也可以根據字段自定義排序
github地址
簡單介紹:在要做查詢的model中添加extend OrderAsSpecified
,
Role.where(id: [2, 1, 5]).order_as_specified(id: [2, 1, 5]).map(&:id) #=> [2, 1, 5]
也可以在此基礎上嵌套排序,具體可以直接看該gem包。
五.如果你沒很多數據要查那最直接的方法。。
[2, 1, 5].map{|id| Role.find(id)}.map(&:id) #=> [2, 1, 5]
***
### 總結
從以上幾種方法里可以看出,當你想要根據特定順序查詢數據時,除了通過ruby的方法來進行排序外,還可以通過各個數據庫的特性來完成排序,當然還需要根據實際情況再做決定,我這里因為繼承關系只能在pg的方法的基礎上再做修改了。。
scope :get_and_order_supplier_cal_popular_country, -> (codes) {
sql = sanitize_sql_array(
["JOIN unnest(array[?]) WITH ORDINALITY t(code, ord) USING (code)
WHERE type = 'Country' ORDER BY t.ord", codes] )
joins(sql)
}
最后,可能有些特殊情況沒有考慮進去,歡迎討論。