Complex Sorting in ActiveRecord
Bài đăng này đã không được cập nhật trong 7 năm
1. Vấn đề khi sắp xếp active record Trong quá trình làm việc tôi gặp phải một số vấn đề liên quan đến việc sắp xếp active record. Phương thức order của active record sẽ nhận đầu vào là đoạn string SQL gồm các column và các option như sau:
User.order('name DESC, email')
# => SELECT "users".* FROM "users" ORDER BY name DESC, email
Bảng user của chúng ta giống như một excel spreadsheet và sẽ được sắp xếp ưu tiên tên trước, nếu trùng tên thì sẽ sắp xếp theo email. Kết quả của nó như sau:
| Name | Email |
+---------+---------------+
Andy | e2@gmail.com |
Andy | e3@gmail.com |
Betty | e1@gmail.com |
Tuy nhiên vấn đề xảy ra là đoạn code trên chưa xử lý các giá trị NULL có thể có trong bảng.
Thông thường, giá trị null xuất hiện đầu tiên khi một cột được sắp xếp tăng dần. Tuy nhiên, chúng tôi muốn điều ngược lại, giá trị NULL sẽ xuất hiện cuối cùng khi sắp xếp tăng dần và đầu tiên khi sắp xếp giảm dần. Điều này khá đơn giản vì chúng ta chỉ cần cung cấp thêm điều kiện NULL FIRST hoặc NULL LAST tương ứng.
Nhưng vấn đề nữa đó là các giá trị ko NULL nhưng đó là một string rỗng (""), và chúng tôi muốn xử lý nó như một giá trị NULL. Trường hợp này có thể sử dụng NULLIF(column_name, value). Ví dụ NULLIF(name, '').
Các vấn đề trên có thể giải quyết không khó, tuy nhiên nếu như việc sắp xếp này phải thực hiện nhiều lần và phức tạp hơn thì chúng ta nên tạo ra một đối tượng riêng dùng cho việc sắp xếp và có thể tái sử dụng
2. Đối tượng hỗ trợ sắp xếp active record Ví dụ người sử dụng muốn sắp xếp tên tăng dần, hire_date giảm dần, và location giảm dần, thông qua params sau:
{
"sort_orders": [
{ "by": "name", "direction": "asc" },
{ "by": "hire_date", "direction": "desc"},
{ "by": "location", "direction": "desc"}
]
}
Dựa vào params trên, chúng ta cần tạo ra đoạn mã như sau:
active_records.order(
"name ASC NULLS LAST",
"hire_date DESC NULLS LAST",
"NULLIF(location, '') DESC NULLS FIRST"
)
Chúng ta sẽ tạo ra một đối tượng gọi là OrderBy. Nó sử dụng như sau:
OrderBy.new("location", nulls: :reversed, null_if: "").to_sql
# => "NULLIF(employees.job_location, '') ASC NULLS LAST"
Điều này thực hiện khá dễ và ngắn:
class OrderBy
attr_accessor :direction
def initialize(column, direction: :asc, nulls: nil, null_if: nil)
@column = column
@direction = direction
@nulls = nulls
@null_if = null_if
end
def to_sql
fragments = [column_sql, direction_sql, nulls_sql]
fragments.compact.join ' '
end
private
attr_reader :column, :nulls, :null_if
def column_sql
if null_if
"NULLIF(#{column}, '#{null_if}')"
else
column
end
end
def direction_sql
direction.to_s.upcase
end
def nulls_sql
if nulls == :reversed
asc? ? 'NULLS LAST' : 'NULLS FIRST'
end
end
def asc?
direction.to_s.downcase == 'asc'
end
end
Bây giờ muốn sắp xếp thông qua params thì chúng ta cần gọi đối tượng hỗ trợ sắp xếp OrderBy trong controller như sau:
ORDER_BY_MAP = {
'name' => OrderBy.new('name', nulls: :reversed, null_if: ''),
'location' => OrderBy.new('location', nulls: :reversed, null_if: ''),
'hire_date' => OrderBy.new('hire_date')
}
def index
query.order(*order_by_args)
end
private
def order_by_args
permitted_params[:sort_orders].map do |param|
order_by_object = ORDER_BY_MAP[param[:by]]
order_by_object.direction = param[:direction]
order_by_object.to_sql
end
end
def permitted_params
params.permit(sort_orders: [:by, :direction])
end
Ta thấy nhờ vào đối tượng trên, đoạn code ở controller khá ngắn và rất dễ hiểu, ngoài ra đoạn đối tượng OrderBy còn có thể tái sử dụng nhiều lần để hỗ trợ việc sắp xếp khác.
3. Kết luận Việc tạo ra một đối tượng con hỗ trợ xử lý với trách nhiệm duy nhất giúp nó sẽ dễ dàng kiểm tra, có khả năng tái sử dụng, và giúp những người maintain sau đó dễ đọc và dễ hiểu nó. Mong rằng bài viết có thể sẽ hữu ích cho bạn. Thanks!
All rights reserved