Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm chủ Rails Scope: Tải trước bản ghi hiệu quả

Bài viết này cũng có sẵn bằng tiếng Hàn, nhờ Soonsang Hong!

Scope của Rails làm cho việc tìm bản ghi bạn muốn trở nên dễ dàng:

app/models/review.rb
class Review < ActiveRecord::Base belongs_to :restaurant scope :positive, -> { where("rating > 3.0") } end
irb(main):001:0> Restaurant.first.reviews.positive.count Restaurant Load (0.4ms) SELECT `restaurants`.* FROM `restaurants` ORDER BY `restaurants`.`id` ASC LIMIT 1 (0.6ms) SELECT COUNT(*) FROM `reviews` WHERE `reviews`.`restaurant_id` = 1 AND (rating > 3.0) => 5

Nhưng nếu bạn không cẩn thận với chúng, bạn sẽ làm tổn thương nghiêm trọng hiệu năng ứng dụng của bạn.

Tại sao? Bạn không thực sự có thể preload một scope. Vì vậy nếu bạn thử hiển thị vài nhà hàng với review tích cực của chúng:

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."]

Đúng, đó là truy vấn N+1. Nguyên nhân lớn nhất của ứng dụng Rails chậm.

Bạn có thể sửa điều này khá dễ dàng, tuy nhiên, nếu bạn suy nghĩ về mối quan hệ theo một cách khác.

Chuyển scope thành association

Khi bạn dùng phương thức association của Rails, như belongs_to và has_many, model của bạn thường trông như thế này:

app/models/restaurant.rb
class Restaurant < ActiveRecord::Base has_many :reviews end

Nhưng nếu bạn kiểm tra tài liệu, bạn sẽ thấy chúng có thể làm hơn. Bạn có thể truyền tham số khác cho các phương thức đó và thay đổi cách chúng hoạt động.

scope là một trong những tham số hữu ích nhất. Nó hoạt động giống như scope từ trước:

app/models/restaurant.rb
class Restaurant < ActiveRecord::Base has_many :reviews has_many :positive_reviews, -> { where("rating > 3.0") }, class_name: "Review" end
irb(main):001:0> Restaurant.first.positive_reviews.count Restaurant Load (0.2ms) SELECT `restaurants`.* FROM `restaurants` ORDER BY `restaurants`.`id` ASC LIMIT 1 (0.4ms) SELECT COUNT(*) FROM `reviews` WHERE `reviews`.`restaurant_id` = 1 AND (rating > 3.0) => 5

Bây giờ, bạn có thể preload association mới của bạn với includes:

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."]

Thay vì 6 lệnh SQL, chúng ta chỉ làm hai.

(Dùng class_name, bạn có thể có nhiều association đến cùng đối tượng. Điều này hữu ích khá thường xuyên.)

Về việc lặp lại?

Vẫn có thể có vấn đề ở đây. where("rating > 3.0") bây giờ nằm trên lớp Restaurant của bạn. Nếu sau này bạn thay đổi positive review thành rating > 3.5, bạn sẽ phải cập nhật nó hai lần!

Nó tệ hơn: Nếu bạn cũng muốn lấy tất cả positive review một người từng để lại, bạn sẽ phải lặp lại scope đó ở lớp User, nữa:

app/models/user.rb
class User < ActiveRecord::Base has_many :reviews has_many :positive_reviews, -> { where("rating > 3.0") }, class_name: "Review" end

Nó không rất DRY.

Có một cách dễ dàng để khắc phục, tuy nhiên. Bên trong where, bạn có thể dùng scope positive bạn đã thêm vào lớp Review:

app/models/restaurant.rb
class Restaurant < ActiveRecord::Base has_many :reviews has_many :positive_reviews, -> { positive }, class_name: "Review" end

Bằng cách đó, ý tưởng về cái gì làm cho một review trở thành positive review vẫn chỉ ở một nơi.

Scope tuyệt vời. Ở đúng chỗ, chúng có thể làm cho truy vấn dữ liệu của bạn dễ dàng và vui. Nhưng nếu bạn muốn tránh truy vấn N+1, bạn phải cẩn thận với chúng.

Vì vậy, nếu một scope bắt đầu gây rắc rối cho bạn, hãy gói nó trong association và preload nó. Nó không nhiều công việc hơn, và nó sẽ tiết kiệm cho bạn một đống lệnh SQL.