Một vài cách để viết scope đa dạng hơn

Trong việc code ruby, đôi khi bạn phải viết những đoạn scope, tip nhỏ sau đây hi vọng giúp bạn phần nào trong việc viết scope, giả sử ta có model class_room :

create_table "class_rooms", force: :cascade do |t|
  t.string   "name"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

class ClassRoom < ActiveRecord::Base
end

Scope đơn giản nhất để lấy ra các phần tử có id nằm trong mảng ids:

scope :by_ids, ->ids{where id: ids}

Phức tạp hơn 1 tẹo là lấy ra các phần tử có id thuộc ids và có name thuộc mảng names

scope :by_ids_and_name, ->ids, names{where id: ids, name: names}

Còn trong trường hợp lấy ra các phần tử có id thuộc ids hoặc name thuộc mảng names, ở đây là điều kiện hoặc (OR) nên câu scope là:

scope :by_ids_or_name, ->ids, name{where 'id IN ? OR name IN ?', ids, names}

Nếu như không muốn đưa string vào câu query thì ta dùng ransack (link tham khảo https://github.com/activerecord-hackery/ransack) scope sẽ là:

scope :by_ids_or_names_ransack, ->ids, names do
  ransack(
    g: {
      by_ids_condition: {id_in: ids},
      by_name_condition: {name_in: names}
    }, m: "or").result
end

Chú ý m: “or” là đưa vào điều kiện OR trong câu query

Giả sử bây giờ thêm trường student_names trong class_room

create_table "class_rooms", force: :cascade do |t|
  t.string   "name"
  t.text     "student_names"
  t.datetime "created_at",    null: false
  t.datetime "updated_at",    null: false
end

chứa dãy các name của student trong class_room phân biệt bởi dấu ',', VD 'Nguyen_Van_A, Nguyen_Van_B, Nguyen_Van_C', tên mỗi người được viết liền, scope lấy ra các class_room có tên 1 vài student nằm trong mảng tên các student_names

scope :by_student_names, ->student_names do
  where create_sql_command student_names
end

Tạo thêm method create_sql_command:

class << self
  def create_sql_command student_names
    student_names.map do |student_name|
      [
        "(student_names LIKE '#{student_name}')",
        "(student_names LIKE '#{student_name},%')",
        "(student_names LIKE '%, #{student_name}')",
        "(student_names LIKE '%, #{student_name},%')",
      ]
    end.join " OR "
  end
end

Thực chất đây là xét các trường hợp mà student_names chứa string là 1 phần tử có trong mảng đưa vào. Viết bằng ransack sẽ là:

scope :by_student_names_ransack, ->student_names do
  ransack(g: create_ransack_hash(student_names), m: "or").result
end

Thêm method create_ransack_hash

def create_ransack_hash student_names
  student_names.each_with_index.inject({}) do |ransack_hash, (student_name, index)|
    ransack_hash.merge! "student_name_#{index}_condition" => {student_names_cont: " #{student_name},"}
  end
end

(Đối với trường hợp này trước lúc đẩy data vào cần thêm dấu cách ở đầu và dấu , ở cuối).

Đối với việc sắp xếp, scope quen thuộc là

scope :sort_by_created_at, ->{order created_at: :asc}

Giả sử ta có thêm bảng time_period

class TimePeriod < ActiveRecord::Base
  belongs_to :class_room
end

create_table "time_periods", force: :cascade do |t|
  t.integer  "class_room_id"
  t.datetime "created_at",    null: false
  t.datetime "updated_at",    null: false
end

Để sort class_room theo thời điểm tạo time_period

scope :by_first_time_period, -> do
  joins(:time_periods)
    .where("time_periods.created_at =
      (SELECT Min(time_periods.created_at)
        FROM time_periods WHERE time_periods.class_room_id = class_rooms.id)")
    .order "time_periods.created_at ASC"
end

Dùng ransack thì sẽ là:

scope :by_first_time_period_ransack, -> do
  joins(:time_periods)
    .ransack(g: {a: {name_cont: "name"}}, sorts: 'time_periods_created_at asc').result.group(:id)
end

(Chỗ này thêm 1 option name có chứa chữ 'name')

Cảm ơn bạn đã đọc và hi vọng bài viết giúp ích trong công việc của bạn, nhất là đối với những người tiếp xúc với ruby chưa nhiều như tôi.


All Rights Reserved