Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Rails 스코프를 미리 로드하는 방법

홍순상 덕분에 이 기사는 한국어로도 볼 수 있습니다!

Rails의 범위를 사용하면 원하는 레코드를 쉽게 찾을 수 있습니다.

앱/모델/리뷰.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

하지만 주의하지 않으면 앱 성능이 심각하게 저하됩니다.

왜요? 범위를 미리 로드할 수는 없습니다. 따라서 긍정적인 리뷰를 가진 몇 개의 레스토랑을 보여주려고 하면:

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

네, N+1 쿼리입니다. Rails 앱이 느려지는 가장 큰 원인입니다.

하지만 관계에 대해 다른 방식으로 생각하면 이 문제를 매우 쉽게 해결할 수 있습니다.

범위를 연결로 변환

belongs_to와 같은 Rails 연관 메소드를 사용하는 경우 및 has_many , 모델은 일반적으로 다음과 같습니다.

앱/모델/레스토랑.rb
class Restaurant < ActiveRecord::Base
  has_many :reviews
end

그러나 문서를 확인하면 더 많은 작업을 수행할 수 있음을 알 수 있습니다. 다른 매개변수를 해당 메소드에 전달하고 작동 방식을 변경할 수 있습니다.

scope 가장 유용한 것 중 하나입니다. scope와 동일하게 작동합니다. 이전:

앱/모델/레스토랑.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

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

6번의 SQL 호출 대신 2번만 호출했습니다.

(class_name 사용 , 동일한 개체에 대해 여러 연결을 가질 수 있습니다. 이것은 꽤 자주 유용합니다.)

중복은 어떻습니까?

여기에 여전히 문제가 있을 수 있습니다. where("rating > 3.0") 님은 이제 레스토랑 수업에 있습니다. 나중에 긍정적인 리뷰를 rating > 3.5로 변경한 경우 , 두 번 업데이트해야 합니다!

상황은 더 나빠집니다. 한 사람이 남긴 모든 긍정적인 리뷰도 가져오려면 User 클래스에서도 해당 범위를 복제해야 합니다.

앱/모델/사용자.rb
class User < ActiveRecord::Base
  has_many :reviews
  has_many :positive_reviews, -> { where("rating > 3.0") }, class_name: "Review"
end

별로 건조하지 않습니다.

이 문제를 해결하는 쉬운 방법이 있습니다. where 내부 , positive를 사용할 수 있습니다. 검토 클래스에 추가한 범위:

앱/모델/레스토랑.rb
class Restaurant < ActiveRecord::Base
  has_many :reviews
  has_many :positive_reviews, -> { positive }, class_name: "Review"
end

그렇게 하면 아이디어 리뷰를 긍정적으로 만드는 요소 중 하나는 여전히 한 곳에 있습니다.

범위는 훌륭합니다. 적절한 위치에서 데이터 쿼리를 쉽고 재미있게 만들 수 있습니다. 하지만 N+1 쿼리를 피하고 싶다면 조심해야 합니다.

따라서 범위가 문제를 일으키기 시작하면 연결로 래핑하고 미리 로드하세요. . 더 많은 작업이 필요하지 않으며 많은 SQL 호출을 절약할 수 있습니다.