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 ...
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 name và body. 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 status là nil 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