Scope và class method trong ruby on rails

Nếu là một rails dev chắc các bạn đã biết về scope và class method. Và dường như cả 2 không có nhiều sự khác biệt. Tuy nhiên, trong bài viết này mình muốn chỉ ra một vài điểm khác biệt giữa scope và class method trong rails.

Định nghĩa một scope

Chúng ta có thể định nghĩa scope trong rails 3 theo 2 cách như sau:

class Post < ActiveRecord::Base
  scope :published, where status: "published"
  scope :draft, ->{where status: "draft"}
end

Sự khác nhau cơ bản giữa 2 các gọi này là cách dùng. Biểu thức điều kiện của published scoped sẽ được gọi 1 lần duy nhất khi class được gọi lần đầu tiên, trong khi điều kiện của scope draft sẽ gọi lại mỗi khi scope được thực thi. Chính vì điều này mà trong rails 4, cách khai báo đầu tiên sẽ không còn được sử dụng. Lý do đơn giản là tránh việc gặp lỗi với trường hợp tham số trong điều kiện của scope là Time.

Một ví dụ:

class Post < ActiveRecord::Base
  scope :published_last_week, where("published_at >= ?", 1.week.ago)
end

Điều kiện bên trong scope sẽ không cho ra kết quả như mong đợi. 1.week.ago sẽ được thực hiện lần đầu tiên và sẽ được sử dụng cho những lần tiếp theo mà không được thực hiện lại.

Scope cũng là class method

Bản thân ActiveRecord đã chuyển đổi scope thành class method. Về mặt khái niệm, thực hiện trong rails đơn giản như sau:

def self.scope(name, body)
  singleton_class.send(:define_method, name, &body)
end

chi tiết các bạn có thể xem ở đây: https://github.com/rails/rails/blob/b1879124a82b34168412ac699cf6f654e005c4d6/activerecord/lib/active_record/scoping/named.rb#L154-L159

Giống như một class method cùng với namebody. Giống như sau:

def self.published
  where status: "published"
end

Và tôi nghĩ rằng tại sao hầu hết mọi người nghĩ: "Tại sao tôi dùng scope nếu như đó là một cú pháp của một class method?". Vậy nên, sau đây là một vài ví dụ để chúng ta cùng suy nghĩ về điều đó.

Scope gọi liên tiếp được

Cùng xem xét một ví dụ sau. Người dùng có thể lọc những bài viets bằng trạng thái, sắp xếp chúng theo thứ tự cập nhật. Đơn giản với các scope như sau:

class Post < ActiveRecord::Base
  scope :by_status, -> status {where status: status }
  scope :recent, -> {order "posts.updated_at DESC"}
end

Và chúng ta có thể gọi chúng một cách thoải mái như sau:

Post.by_status(""published"").recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published'
#   ORDER BY posts.updated_at DESC

hoặc thông qua params

Post.by_status(params[:status]).recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published'
#   ORDER BY posts.updated_at DESC

Bây giờ, chúng ta sẽ mô phỏng lại ví dụ trên thông qua class method, đề từ đó có sự so sánh với scope như sau:

class Post < ActiveRecord::Base
  class << self
    def by_status status
      where status: status
    end

    def recent
      order "posts.updated_at DESC"
    end
  end
end

Cùng xem vấn đề xảy ra của chúng ta là gì khi sử dụng với statusnil hoặc blank?

Post.by_status(nil).recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" IS NULL
#   ORDER BY posts.updated_at DESC

Post.by_status('').recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" = ''
#   ORDER BY posts.updated_at DESC

Có vẻ ổn, nhưng tôi nghĩ rằng không cho phép việc query dữ liệu với 2 điều kiện trên. Chúng ta thay đổi scope đã định nghĩa bên trên một chút như sau:

  scope :by_status, -> status {where status: status if status.present?}

Thử lại với scope:

Post.by_status(nil).recent
# SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC

Post.by_status('').recent
# SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC

Tuyệt vời, mọi thứ vẫn hoạt động tốt. Bây giờ thay đổi class method và thử lại xem sao:

class Post < ActiveRecord::Base
  class << self
    def by_status status
      where status: status if status.present?
    end
  end
end

Kết quả

Post.by_status("").recent
NoMethodError: undefined method `recent' for nil:NilClass

Chuyện gì xảy ra vậy? Có sự khác nhau ở đây. Scope thì luôn luôn trả về một ActiveRecord Relation, trong khi class method thì không hoạt động. Và để class method hoạt động được, chúng ta thay đổi một chút như sau:

  class << self
    def by_status status
      if status.present?
        where status: status
      else
        all
      end
    end
  end

Chú ý, chúng ta trả về all cho trường hợp nil/blank. Trong rails 4 thì sẽ trả về một relation thay vì 1 array như rails 3. Lời khuyên ở đây là: đừng bao giờ trả kết quả về nil với class method nếu không thì bạn đang phá với các điều kiện bao hàm bởi scope, luôn luôn trả về một relation.

Scope mở rộng được

Cùng thực hiện phân trang trong ví dụ tiếp theo và chúng ta sẽ sử dụng [Kaminari] gem. Điều quan trọng bạn cần làm khi phân trang một tập hợp dữ liệu là số trang bạn muốn lấy dữ liệu:

Post.page(2)

Sau đó là có thể lấy bao nhiêu bản ghi mỗi trang mà chúng ta muốn

Post.page(2).per(15)

Và bạn cũng muốn biết tổng số trang hoặc trang đang dùng là đầu hay cuối:

posts = Post.page(2)
posts.total_pages # => 2
posts.first_page? # => false
posts.last_page?  # => true

Điều này có ý nghĩa khi chúng ta gọi chúng theo thứ tự, nhưng cũng chẳng có ý nghĩa gì cả khi gọi những methods đó chưa phần trang. Khi viết scope, chúng ta có thể thêm các thành phần mở rộng bên trong scope và những thành phần mở rộng này chỉ có tác dụng với object nếu như scope được gọi. Trong trường hợp của kaminari, các thành phần mở rộng được gọi khi mà page được gọi. Chúng ta có thể mô tả lại bằng code như sau:

scope :page, -> num { # some limit + offset logic here for pagination } do
  def per(num)
    # more logic here
  end

  def total_pages
    # some more here
  end

  def first_page?
    # and a bit more
  end

  def last_page?
    # and so on
  end
end

Mở rộng scope là một kỹ thuật mạnh mẽ và mềm dẻo. Tuy nhiên để xử lý với class method chúng ta cũng có thể làm được như sau:

def self.page(num)
  scope = # some limit + offset logic here for pagination
  scope.extend PaginationExtensions
  scope
end

module PaginationExtensions
  def per(num)
    # more logic here
  end

  def total_pages
    # some more here
  end

  def first_page?
    # and a bit more
  end

  def last_page?
    # and so on
  end
end

Và kết quả trả về tương tự như scope. Lời khuyên dành cho các bạn là: biết cái gì là tốt hơn nhưng không quên những thứ căn bản trước khi có điều đó.

Trên đây là một vài điểm nhỏ khác nhau giữa scope và class methods trong rails. Tùy thuộc vào mục đích để các bạn sử dụng scope hoặc class method.

Tham khảo: http://blog.plataformatec.com.br/2013/02/active-record-scopes-vs-class-methods/ http://www.justinweiss.com/articles/should-you-use-scopes-or-class-methods/ https://isotope11.com/blog/scopes-are-just-class-methods-and-consequences-of-thinking-this-through