07/09/2018, 16:58

Should you use scopes or class methods?

Scopes là rút gọn của các câu truy vấn cơ sở dữ liệu trong Rails, cũng giống như where, chúng được sử dụng thường xuyên khi chúng ta muốn lấy ra các objects thích hợp từ database Như ví dụ sau: app/models/review.rb class Review < ActiveRecord::Base scope :most_recent , -> ...

Scopes là rút gọn của các câu truy vấn cơ sở dữ liệu trong Rails, cũng giống như where, chúng được sử dụng thường xuyên khi chúng ta muốn lấy ra các objects thích hợp từ database

Như ví dụ sau:

app/models/review.rb

class Review < ActiveRecord::Base
  scope :most_recent, -> (limit) { order("created_at desc").limit(limit) }
end

Chúng ta thường sử dụng scope như thế này:

app/models/homepage_controller.rb

@recent_reviews = Review.most_recent(5)

Việc gọi scope nhìn giống hệt như gọi một class method của model Review vậy. Hơn nữa, không chỉ dừng lại ở cách sử dụng, chúng ta hoàn toàn có thể xây dựng một "scope" dưới dạng một class method:

app/models/review.rb

def self.most_recent(limit)
  order("created_at desc").limit(limit)
end

app/controllers/homepage_controller.rb

@recent_reviews = Review.most_recent(5)

Vậy tại sao chúng ta lại sử dụng scope khi mà hoàn toàn có thể sử dụng các class methods thông thường của Ruby? Có đáng để giữ lại 2 thứ riêng rẽ như vậy hay tất cả chỉ để làm cho Rails khó học hơn ?

Tại sao lại dùng scope khi đã có class method ?

Giả sử bạn muốn lấy tất cả các review được viết sau một ngày cụ thể ? Trong trường hợp không chỉ rõ một ngày nào, tất cả các review sẽ được lấy ra

Với scope, mọi thứ sẽ trông giống như thế này:

app/models/review.rb

scope :created_since, ->(time) { where("reviews.created_at > ?", time) if time.present? }

Easy enough, right? Còn class method thì sao?

app/models/review.rb

def self.created_since(time)
  if time.present?
    where("reviews.created_at > ?", time)
  else
    all
  end
end

Để một class method hoạt động tương tự, chúng ta phải thêm xử lý cho trường hợp time nil. Mặt khác, caller cũng phải xác định trường hợp nó có một chainable scope hợp lệ.

Scope luôn trả ra kết quả là scopes, do đó chúng rất dễ để xâu chuỗi lại với nhau:

Review.positive.created_since(5.days.ago)


Methods that always return the same kind of object are really useful.

Chúng ta không cần phải lo lắng nhiều về các trường hợp biên cũng như lỗi. Bạn có thể cho rằng mình sẽ luôn luôn thu được một object mà bạn có thể sử dụng được.
Ở đây, điều đó có nghĩa bạn luôn có thể nối các scope lại với nhau, mà không phải lo lắng về các giá trị nil.

Dù vậy vẫn tồn tại một số cách để bạn có thể phá vỡ giả thuyết rằng bận luôn luôn thu về được một scope khi nối chúng:

app/models/review.rb

scope :broken, -> { "Hello!!!" }
irb(main):001:0> Review.broken.most_recent(5)
NoMethodError: undefined method `most_recent' for "Hello!!!":String

Lỗi này đã bao giờ xảy ra trong project của bạn chưa ?

Điều khiến tôi yêu thích nhất ở scope chính là chúng diễn tả mục địch một cách rõ ràng. Khi sử dụng scope bạn đang nói cho người đọc code của mình rằng "method này có thể được móc nối, sẽ đưa ra một list các object, và sẽ giúp bạn chọn ra được một tập hợp các object thỏa mãn yêu cầu nào đó. Điều đó mang lại nhiều thông điệp hơn là một class method thông thường truyền đạt.

Khi nào nên sự dụng class method thay vì scope?

Bởi vì scope diễn tả mục đích, tôi sử dụng chúng bất cứ khi nào muốn móc nối các chuỗi đơn giản, built-in (where và limit) vào các scope phức tạp hơn. Tìm ra một tập các object thỏa mã chính là nhiệm vụ mà scope hướng tới.

Tuy nhiên có hai ngoại lệ:

  • Khi muốn preload scope, tôi chuyển chúng thành association.
  • Khi muốn xử lý nhiều hơn việc nối built-in scope vào một scope lớn hơn, class method là lựa chọn tốt hơn.

Logic của scope thỉnh thoảng sẽ trở nên phức tạp, lúc đó class method như là một nơi đúng đắn để bạn đặt code của mình vào đó. Trong class method, bạn có thể dễ dàng mix Ruby code và database code lại với nhau. Ví dụ bạn có một class method thu thập dữ liệu từ một vài nguồn khác nhau như: database, Redis hay một API/service bên ngoài, sau đó, tập trung lại trong một collection giống như scope - cuối cùng sẽ được chuyển thành một mảng dữ liệu.

Ngay cả lúc đó, bạn vẫn có thể thêm vào selecting, sorting, joining và filtering code bên trong các scope và sử dụng chúng một cách bình thường trong class method. Cuối cùng bạn sẽ có một class method rõ ràng hơn, xử lý những logic phức tạp mà vẫn tận dụng được các scope sẵn có.

Ngoài ra, hãy thử tưởng tượng model được kế thừa một scope từ model cha nhưng yêu cầu phải sửa đổi một vài điểm nhỏ ( thêm biến, thêm xử lý logic, thêm sort hay filter, ...) và bạn sẽ lại viết lại scope mới ý nguyên như trong model cha với 1 chút thay đổi. Hãy dùng class method và super nhé.

Cuối cùng hãy ghi nhớ rằng:

Scope for chaining and simple logic method ex: sort, filter record, class method for doing work beyond that.

0