Scope trong rails cách sử dụng và điểm khác biệt giữa class method

Xin chào các bạn. Hôm nay mình xin viết về Scope trong ruby on rails. Các scope được hỗ trợ bởi rails, giúp định nghĩa các điều kiện truy vấn, chúng ta có thể kết nối nhiều scope với nhau mà không tạo ra nhiều câu truy vấn. Về bản chất thì scope là 1 class method (có thể gọi là class method động)

class Shirt < ActiveRecord::Base
  def self.red
    where(color: 'red')
  end
end

mình có thể viết scope thành:

class Shirt < ActiveRecord::Base
  scope :red, -> {where(color: 'red')}
end

Trước khi nói đến sự khác nhau giữa scope và class method mình xin được nói đến 1 vài cách viết scope đơn giản nhất

Đầu tiên ta có 1 model HistoryUpload

class CreateHistoryUploads < ActiveRecord::Migration
  def change
    create_table :history_uploads do |t|
      t.integer :type_id
      t.string :file_path

      t.timestamps null: false
    end
  end
end

class HistoryUpload < ActiveRecord::Base
end

Lấy ra những record type_id có trong bảng history_uploads

scope :search_type_id, -> type_id {where type_id: type_id}

Lấy những record type_id có file_path trong bảng history_uploads

scope :search_type_id_and_type_path, -> type_id, type_path {where type_id: type_id, type_path: type_path}

Điều kiện OR trong scope

scope :search_type_id_or_type_path, -> type_id, type_path {'type_id IN ? OR type_path IN ?' type_id, type_path}

hoặc có thể viết ntn nếu không thích viết chuỗi string trong scope

scope :search_type_id_or_type_path, ->type_id, type_path do
  ransack(
    g: {
      type_id: {type_id_in: type_id},
      type_path: {type_path_in: file_path}
    }, m: "or").result
end

Với việc sắp xếp bằng scope: đơn giản như:

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

dùng scope để join bảng thì giờ mình sẽ tạo 1 bảng nữa : file_upload

class CreateFileUploads < ActiveRecord::Migration
  def change
    create_table :file_uploads do |t|
      t.integer :history_upload_id

      t.timestamps null: false
    end
  end
end

class FileUpload < ActiveRecord::Base
    belongs_to :history_upload
end

scope :join_historyupload, -> {joins(:history_uploads)}

Vậy tại sao sử dụng scope thay class method. cho dù nó chỉ là cách viết khác của class method. Tất nhiên khi sử dụng scope sẽ có cái khác so với class method.

Đầu tiên phải nói đến method chain luôn luôn được đảm bảo thực hiện.

Giả sử ta sẽ so sánh scope:

#scope
class HistoryUpload < ActiveRecord::Base
  scope :search_type_id, -> type_id {where type_id: type_id}
end
#class_method
class HistoryUpload < ActiveRecord::Base
  def self.search_type type_id
    where(type_id: type_id)
  end
end

kết quả chúng ta thu được là khi type_id là nil or ""

#scope
 HistoryUpload Load (0.2ms)  SELECT `history_uploads`.* FROM `history_uploads` WHERE `history_uploads`.`type_id` = NULL
 => #<ActiveRecord::Relation []>
#class_method
 HistoryUpload Load (0.2ms)  SELECT `history_uploads`.* FROM `history_uploads` WHERE `history_uploads`.`type_id` = NULL
 => #<ActiveRecord::Relation []>

Vậy để đảm bảo khả năng method_chain ta sẽ thêm điều kiện type_id.present?

#scope
class HistoryUpload < ActiveRecord::Base
  scope :search_type_id, -> type_id {where type_id: type_id type_id.present?}
end
#class_method
class HistoryUpload < ActiveRecord::Base
  def self.search_type type_id
    where(type_id: type_id) type_id.present?
  end
end

kết quả thu được khi chúng ta đã thay đổi

#scope
SELECT `history_uploads`.* FROM `history_uploads`
 => #<ActiveRecord::Relation [#<HistoryUpload id: 46, type_id: 1,....]
#class_method
nil

KQ: Với cách viết như trên, các scope chắc chắn sẽ thực hiện chain mà không gặp phải vấn đề gì. Tức là bạn luôn luôn thu được object chứ ko phải 1 giá trị nil

1 điều nữa là scope luôn luôn diễn đạt mục đích 1 cách rõ ràng mang lại nhiều thông điệp hơn 1 class method.

điểm hạn chế của scope là nhiều lúc sẽ trở nên phức tạp, rắc rối hơn. khi viết code của mình trong 1 class method tường tận và rõ ràng.

thử hình dùng 1 scope có cả sort, select, join hay được viết mạnh lạc trong class method và có thể xử lý những logic phức tập + sử dụng được những scope sẵn có.

scope :test_scope, -> file_path { joins(:history_uploads).where('file_path: file_path').order("created_at: :desc") }

Cá nhân mình nghĩ việc sử dụng class method or scope là do mỗi người. vì nhìn chung scope cũng chỉ là 1 class method mà thôi.


All Rights Reserved