홍순상 덕분에 이 기사는 한국어로도 볼 수 있습니다!
Rails의 범위를 사용하면 원하는 레코드를 쉽게 찾을 수 있습니다.
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
, 모델은 일반적으로 다음과 같습니다.
class Restaurant < ActiveRecord::Base
has_many :reviews
end
그러나 문서를 확인하면 더 많은 작업을 수행할 수 있음을 알 수 있습니다. 다른 매개변수를 해당 메소드에 전달하고 작동 방식을 변경할 수 있습니다.
scope
가장 유용한 것 중 하나입니다. scope
와 동일하게 작동합니다. 이전:
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 클래스에서도 해당 범위를 복제해야 합니다.
class User < ActiveRecord::Base
has_many :reviews
has_many :positive_reviews, -> { where("rating > 3.0") }, class_name: "Review"
end
별로 건조하지 않습니다.
이 문제를 해결하는 쉬운 방법이 있습니다. where
내부 , positive
를 사용할 수 있습니다. 검토 클래스에 추가한 범위:
class Restaurant < ActiveRecord::Base
has_many :reviews
has_many :positive_reviews, -> { positive }, class_name: "Review"
end
그렇게 하면 아이디어 리뷰를 긍정적으로 만드는 요소 중 하나는 여전히 한 곳에 있습니다.
범위는 훌륭합니다. 적절한 위치에서 데이터 쿼리를 쉽고 재미있게 만들 수 있습니다. 하지만 N+1 쿼리를 피하고 싶다면 조심해야 합니다.
따라서 범위가 문제를 일으키기 시작하면 연결로 래핑하고 미리 로드하세요. . 더 많은 작업이 필요하지 않으며 많은 SQL 호출을 절약할 수 있습니다.