Scopes hay Class method
Bài đăng này đã không được cập nhật trong 6 năm
Khi review code của mọi người, đặc biệt là một số new member, tôi thường thấy mọi người hay bị hiểu sai tác dụng của scope
và thường viết scope như viết class method
, trường hợp ngược lại thì ít gặp hơn. Câu hỏi đặt ra là lúc nào thì viết scope
, lúc nào thì viết class method
? Bài viết này sẽ trả lời câu hỏi đó.
Scope là gì ?
Scope
là một cách tuyệt vời để lấy các object từ database.
Với RoR, chúng ta có thể viết như sau:
# app/models/review.rb
class Review < ActiveRecord::Base
scope :most_recent, -> (limit) { order("created_at desc").limit(limit) }
end
Để gọi scope ta có thể gọi như sau: @recent_reviews = Review.most_recent(5)
Cách gọi scope khá giống với gọi class method
và trên thực tế chúng ta hoàn toàn có thể viết class method giống như viết scope:
# app/models/review.rb
def self.most_recent(limit)
order("created_at desc").limit(limit)
end
Vậy tại sao chúng ta phải sử dụng scope
trong khi có thể dùng class method
Tại sao phải sử dụng scope
?
Hãy thử một yêu cầu sau: Hãy lấy tất cả những reviews được viết vào một ngày nào đó, nhưng nếu không chỉ định ngày nào thì sẽ lấy ra tất cả.
Nếu dùng scope
sẽ viết như sau:
# app/models/review.rb
scope :created_since, ->(time) { where("reviews.created_at > ?", time) if time.present? }
Còn nếu dùng 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
Rõ ràng nếu dùng scope
chúng ta sẽ viết ngắn gọn hơn rất nhiều. Hơn nữa, scope
được thiết kế để bắt buộc phải trả về scope
vì vậy chúng dễ dàng được gắn kết với nhau
Review.scope_1.scope_2...
Kể cả trong trường hợp, scope
của bạn vì một lý do nào đó không trả về một scope
thì sẽ luôn có lỗi được thông báo.
# app/models/review.rb
scope :broken, -> { "Hello!!!" }
-------------------------------
irb(main):001:0> Review.broken.most_recent(5)
NoMethodError: undefined method `most_recent' for "Hello!!!":String
Nhưng với class method
thì không làm việc như vậy, bạn phải tự xử lý trường hợp mà method không trả về một scope
phù hợp (trường hợp trả về nil
chẳng hạn) nếu muốn các class method
có thể gắn kết với nhau.
Tuy nhiên không phải lúc nào cũng nên sử dụng scope
, có những trường hợp chúng ta nên sử dụng class method
thay cho scope
Khi nào nên dùng class method
thay thế scope
?
Có 2 trường hợp nên sử dụng class method
:
- Khi cần preload scopes, chúng ta sẽ chuyển scope thành associations thay vì dùng trực tiếp scope
- Khi cần làm nhiều việc hơn là đơn giản chỉ là nối những scope nhỏ thành một scope lớn, ta sẽ sử dụng
class method
.
Trường hợp 1:
Giả sử chúng ta có một model như sau:
# app/models/review.rb
class Review < ActiveRecord::Base
belongs_to :restaurant
scope :positive, -> { where("rating > 3.0") }
end
Nếu dùng trực tiếp scope
để lấy những review positive thì sẽ gặp N+1 query
, nó sẽ làm chậm Rails app
irb(main):001:0> restauraunts = Restaurant.first(5)
irb(main):002:0> restauraunts.map do |restaurant|
irb(main):003:1* "#{restaurant.name}: #{restaurant.reviews.positive.length} positive reviews."
irb(main):004:1> end
Review Load (0.6ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 1 AND (rating > 3.0)
Review Load (0.5ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 2 AND (rating > 3.0)
Review Load (0.7ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 3 AND (rating > 3.0)
Review Load (0.7ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 4 AND (rating > 3.0)
Review Load (0.7ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 5 AND (rating > 3.0)
=> ["Judd's Pub: 5 positive reviews.", "Felix's Nightclub: 6 positive reviews.", "Mabel's Burrito Shack: 7 positive reviews.", "Kendall's Burrito Shack: 2 positive reviews.", "Elisabeth's Deli: 15 positive reviews."]
Vì vậy chúng ta phải chuyển scope
thành associations
như sau để tránh trường hợp N+1 query
:
# app/models/restaurant.rb
class Restaurant < ActiveRecord::Base
has_many :reviews
has_many :positive_reviews, -> { positive }, class_name: "Review"
end
irb(main):001:0> restauraunts = Restaurant.includes(:positive_reviews).first(5)
Restaurant Load (0.3ms) SELECT `restaurants`.* FROM `restaurants` ORDER BY `restaurants`.`id` ASC LIMIT 5
Review Load (1.2ms) SELECT `reviews`.* FROM `reviews` WHERE (rating > 3.0) AND `reviews`.`restaurant_id` IN (1, 2, 3, 4, 5)
irb(main):002:0> restauraunts.map do |restaurant|
irb(main):003:1* "#{restaurant.name}: #{restaurant.positive_reviews.length} positive reviews."
irb(main):004:1> end
=> ["Judd's Pub: 5 positive reviews.", "Felix's Nightclub: 6 positive reviews.", "Mabel's Burrito Shack: 7 positive reviews.", "Kendall's Burrito Shack: 2 positive reviews.", "Elisabeth's Deli: 15 positive reviews."]
Trường hợp 2:
Khi logic trong scope của bạn quá phức tạp, thì class method
là một lựa chọn tốt hơn. Bởi:
- Với một
class method
, bạn có thể dễ dàng đưa thêm code Ruby khác ngoài code liên quan tới database. Ví dụ: nếu cần đoạn codesorting
mà nó dễ dàng hơn nếu viết bằng Ruby thì ta hoàn toàn có thể lấy objects bằng code database và dùngsort_by
của Ruby để sắp xếp lại
# app/models/review.rb
def self.created_since(time)
if time.present?
where("reviews.created_at > ?", time)
else
all
end.limit(100).sort_by{|_review| _review.created_at}
end
- Hoặc sẽ tốt hơn, nếu sử dụng
class method
để lấy data từ các nguồn khác nhau: database, Redis, hoặc API hay service.
Tham khảo
https://www.justinweiss.com/articles/should-you-use-scopes-or-class-methods/ https://www.justinweiss.com/articles/how-to-preload-rails-scopes//
All rights reserved