Trong Rails, dễ dàng lấy một đống bản ghi từ cơ sở dữ liệu nếu bạn có ID của chúng:
Person.where(id: [1, 2, 3]).map(&:id) => [1, 2, 3]
Nhưng nếu bạn muốn lấy bản ghi về theo thứ tự khác? Có thể search engine trả về ID liên quan nhất trước. Làm sao bạn giữ bản ghi theo thứ tự đó?
Bạn có thể thử where lại:
Person.where(id: [2, 1, 3]).map(&:id) => [1, 2, 3]
Nhưng đó hoàn toàn không hoạt động. Vậy bạn làm sao lấy bản ghi về đúng thứ tự?
Cách tương thích: câu lệnh case
Giống như Ruby, SQL hỗ trợ câu lệnh case...when.
Bạn không thấy nó thường, nhưng câu lệnh case...when có thể gần như hành động như hash. Bạn có thể map một giá trị sang giá trị khác:
case :b when :a then 1 when :b then 2 when :c then 3 end # => 2
Câu lệnh case đó trông giống hash:
{ :a => 1, :b => 2, :c => 3 }[:b] # => 2
Vì vậy, bạn có cách map key sang thứ tự chúng nên xuất hiện. Và cơ sở dữ liệu có thể sắp xếp và trả về kết quả theo thứ tự tùy ý đó.
Biết điều đó, bạn có thể đặt ID và vị trí của chúng vào câu lệnh case, và dùng nó trong mệnh đề SQL order.
Vì vậy nếu bạn muốn đối tượng trả về theo thứ tự [2, 1, 3], SQL của bạn có thể trông như thế này:
SELECT * FROM people WHERE id IN (1, 2, 3) ORDER BY CASE id WHEN 2 THEN 0 WHEN 1 THEN 1 WHEN 3 THEN 2 ELSE 3 END;
Bằng cách đó, bản ghi của bạn được trả về đúng thứ tự. CASE biến đổi mỗi ID thành thứ tự nó nên được trả về.
Tất nhiên, trông nó rối rắm. Và bạn có thể tưởng tượng mệnh đề như đó sẽ phiền toái như thế nào để xây dựng thủ công.
Nhưng bạn không phải xây dựng nó thủ công. Đó là việc của Ruby:
module Extensions::ActiveRecord::FindByOrderedIds extend ActiveSupport::Concern module ClassMethods 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 end end ActiveRecord::Base.include(Extensions::ActiveRecord::FindByOrderedIds) Person.find_ordered([2, 1, 3]) # => [2, 1, 3]
Chính xác như chúng ta muốn!
Cách MySQL-specific sạch hơn
Nếu bạn dùng MySQL, có cách sạch hơn để làm điều này. MySQL có cú pháp đặc biệt ORDER BY FIELD:
SELECT * FROM people WHERE id IN (1, 2, 3) ORDER BY FIELD(id, 2, 1, 3);
Bạn cũng có thể generate nó từ Ruby:
module Extensions::ActiveRecord::FindByOrderedIds extend ActiveSupport::Concern module ClassMethods def find_ordered(ids) sanitized_id_string = ids.map {|id| connection.quote(id)}.join(",") where(id: ids).order("FIELD(id, #{sanitized_id_string})") end end end ActiveRecord::Base.include(Extensions::ActiveRecord::FindByOrderedIds)
Vì vậy, nếu bạn dùng MySQL, và không quá lo về tương thích, đây là cách tốt để đi. Nó dễ đọc hơn nhiều khi những câu lệnh bay qua log của bạn.
Khi bạn muốn hiển thị bản ghi theo thứ tự cụ thể, tùy ý, bạn không cần sắp xếp chúng trong Ruby. Với một đoạn mã nhỏ, bạn có thể để cơ sở dữ liệu làm những gì nó giỏi: tìm, sắp xếp, và trả dữ liệu về ứng dụng của bạn.