12/08/2018, 14:38

Complex Sorting in ActiveRecord

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".* ...

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!

0