Tìm hiểu về Scope và Class method trong Ruby
1. Khái niệm Scopes là cách viết rút gọn của câu truy vấn dữ liệu trong Rails. Chúng được sử dụng thường xuyên khi chúng ta muốn lấy ra các đối tượng dữ liệu từ cơ sở dữ liệu. Ví dụ về scope: scope :published , - > { where ( status : "published" ) } Về bản chất, Rails ...
1. Khái niệm
Scopes là cách viết rút gọn của câu truy vấn dữ liệu trong Rails. Chúng được sử dụng thường xuyên khi chúng ta muốn lấy ra các đối tượng dữ liệu từ cơ sở dữ liệu.
Ví dụ về scope:
scope :published, -> { where(status: "published") }
Về bản chất, Rails sẽ convert scope thành một class method. Ví dụ scope trên sẽ được chuyển thành
def self.pulished where(status: "published") end
Vậy tại sao chúng ta lại sử dụng scope trong khi đã có class method? Sau đây là các lý do thú vị.
2. Scopes luôn luôn có thể kết nối
Giải sử chúng ta có hai scope như sau:
class Post < ActiveRecord::Base scope :by_status, -> status { where(status: status) } scope :recent, -> { order("posts.updated_at DESC") } end
Chúng được sử dụng như sau:
Post.by_status("published").recent # SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published' # ORDER BY posts.updated_at DESC
Bây giờ chúng ta thử chuyển chúng về dạng class method
class Post < ActiveRecord::Base def self.by_status status where(status: status) end def self.recent order("posts.updated_at DESC") end end
Trong đa số trường hợp, có vẻ như class method cũng đảm bảo khả năng kết nối được. 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
Nhưng nếu là scope, chúng ta có thể dễ dàng xử lý bằng cách:
scope :by_status, -> status { where(status: status) if status.present? }
Khi đó với tham số nil chúng ta có:
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
Vậy với class method thi sao? Khi đó code được viết lại 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
Như chúng ta thấy, sự khác biệt ở đây là scopes sẽ luôn luôn trả về một mối quan hệ. Trong trường hợp những query phát sinh trong scope là nil, scope sẽ trả về .all. Vì thế quá trình kết nối vẫn diễn ra bình thường. Để không phát sinh lỗi, class method cần được viết lại như sau:
def self.by_status(status) if status.present? where(status: status) else all end end
3. Scopes có thể mở rộng được
Lấy ví dụ về việc phân trang (pagination) sử dụng gem kaminari. Chúng ta thường sử dụng các phương thức sau:
Post.page(2).per(15) posts = Post.page(2) posts.total_pages # => 2 posts.first_page? # => false posts.last_page? # => true
Trong một văn cảnh độc lập, việc gọi các phương thức per, total_pages, first_page, last_page sẽ dẫn đến khó hiểu. Tuy nhiên nếu trước đó chúng ta gọi phương thức page thì mọi việc trở nên rõ ràng. Trong trường hợp này của gem kaminari, người ta chỉ cần viết một scope là page, sau đó trong phần mở rộng của scope sẽ viết các phương thức còn lại.
scope :page, -> num { # some limit + offset logic here for pagination } do def per num end def total_pages end def first_page? end def last_page? end end
Với 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
Hai cách trên đều cho ra kết quả giống nhau và chúng ta hoàn toàn có thể sử dụng một trong hai cách đó trong dự án thực tế. Tuy nhiên cần sử dụng chúng theo đúng mục đích vào công việc tương ứng với công cụ bạn đang sử dụng. Cá nhân tôi thường sử dụng scope với những truy vấn yêu cầu logic không quá phức tạp hoặc cần sự kết nối nhiều truy vấn với nhau. Class method sẽ dành cho những công việc yêu cầu độ khó logic nhiều hơn, yêu cầu nhiều tham số đầu vào hơn.