Vì sao lại nên dùng scope hơn class method ?
Scope được dùng khá phổ biến trong Rails. Scope khá giống với class methods khiến nhiều bạn nhầm lẫn, vậy scope là gì và sử dụng như thế nào cho đúng? Scope là một phần được support bởi Active Record. Scope thường định nghĩa các query dùng chung và có thể gọi từ association objects hoặc model. Về ...
Scope được dùng khá phổ biến trong Rails. Scope khá giống với class methods khiến nhiều bạn nhầm lẫn, vậy scope là gì và sử dụng như thế nào cho đúng?
Scope là một phần được support bởi Active Record. Scope thường định nghĩa các query dùng chung và có thể gọi từ association objects hoặc model. Về bản chất thì scope là class method được định nghĩa dưới dạng động thông qua một method tên là scope dùng để định nghĩa ra các scope khác nhau. Thông qua một vài ví dụ dưới đây hi vọng bạn có thể hiểu rõ hơn về scope.
1. Bản chất của scope và class method
Bản chất thì scope cũng chính là class method, trong Rails thì scope được định nghĩa như là 1 class method động.
def self.scope(name, body) singleton_class.send(:define_method, name, &body) end
Ví dụ như với scope dưới đây :
scope :published, -> {where(status: 'published')}
Scope này sẽ được triển khai thành :
def self.published where(status: 'published') end
Về mặt bản chất, scope là 1 class method. Vậy tại sao lại nên dùng scope hơn class method ? Dưới đây là 2 lý do :
- Scope luôn đảm bảo sẽ thực hiện method chain
Giả sử ta có 2 scope như sau :
class Post < ActiveRecord::Base scope :by_status, -> status { where(status: status) } scope :recent, -> { order("posts.updated_at DESC") } end
Như trong model này, có vẻ như viết dưới dạng class method cũng vẫn đảm bảo khả năng method chain. Tuy nhiên, trong trường hợp tham số status là nil hoặc blank thì sẽ thế nào ?
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`
Thông thường, nếu ta không muốn phát sinh ra những query như thế thì ta sẽ sửa thành :
scope :by_status, -> status { where(status: status) if status.present? }
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ì.
Vậy với class method thì sao ? Code lúc ấy sẽ như sau :
class Post < ActiveRecord::Base def self.by_status(status) where(status: status) if status.present? end end Post.by_status(').recent NoMethodError: undefined method 'recent' for nil:NilClass
Trong trường hợp những query phát sinh trong scope là nil, scope sẽ trả về .all, vì thế mà quá trình chain vẫn diễn ra bình thường.
- Scope có thể mở rộng được Trong các thư viện pagination, cách viết như sau thường được sử dụng :
Post.page(2).per(15) posts = Post.page(2) posts.total_pages # => 2 posts.first_page? # => false posts.last_page? # => true
Các method như .per, total_pages, first_page?, last_pages?, … ta chỉ muốn gọi lên sau khi đã gọi .page. Khi đó ta sẽ viết dưới dạng scope extensions.
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
Với cùng mục đích như vậy, nếu viết dưới dạng class method sẽ 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
- Kết luận
Như chúng ta thấy rõ ràng viết bằng scope có nhiều ưu điểm hơn cách viết bằng class method. Tuy nhiên chúng ta không nên quá lạm dụng nó. Quan trọng nhất là sử dụng với đúng mục đích và yêu cầu của công việc