Tìm hiểu về Scope và Class method trong Ruby
Bài đăng này đã không được cập nhật trong 8 năm
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.
Tài liệu tham khảo:
http://blog.plataformatec.com.br/2013/02/active-record-scopes-vs-class-methods/
http://kipalog.com/posts/Funny-things-about-Rails-scope-and-class-method
All rights reserved