12/08/2018, 13:28

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.

0