Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ Rails Scopes: Preload Records ให้มีประสิทธิภาพ

บทความนี้มีภาษาเกาหลีด้วย ขอบคุณ Soonsang Hong!

Scopes ของ Rails ทำให้หา records ที่ต้องการง่าย:

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

แตถ้าไม่ระวัง จะทำลาย performance ของแอปอย่างรุนแรง

ทำไม? Preload scope ไม่ได้จริงๆ ถ้าพยายามแสดงหลาย restaurants พร้อม positive reviews:

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 query สาเหตุใหญ่ที่สุดของ Rails apps ช้า

แตแก้ได้ง่าย ถ้าคิดถึง relationship แบบต่างออกไป

แปลง Scopes เป็น Associations

ตอน用 Rails association methods เช่น belongs_to และ has_many model มักเป็นแบบนี้:

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

แตถ้าเช็ค documentation จะเห็นว่าทำได้มากกว่านี้ ส่ง parameters อื่นๆ เข้าไปเปลี่ยน behavior ได้

scope เป็นหนึ่งใน parameters ที่ useful ที่สุด ทำงานเหมือน scope ตอนแรก:

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

ตอนนี้ preload association ใหม่ได้ด้วย 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 calls กลายเป็น 2 calls

(用 class_name สร้าง multiple associations ไป object เดียวกันได้ ใช้บ่อย)

เรื่อง Duplication

อาจยังมีปัญหา where("rating > 3.0") อยู่บน Restaurant class ถ้าต่อไปเปลี่ยน positive reviews เป็น rating > 3.5 ต้อง update สองที่!

แย่กว่า: ถ้าอยากดึง positive reviews ทั้งหมดที่คนเคยทิ้งไว้ ต้อง duplicate scope ไป User class ด้วย:

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

ไม่ DRY เลย

มีวิธีหลีกเลี่ยงง่าย ภายใน where ใช้ positive scope ที่เพิ่มไว้ใน Review class:

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

วิธีนี้ "ความคิด" ว่าอะไรทำให้ review เป็น positive อยู่ที่เดียว

Scopes ดี ตำแหน่งที่ถูกต้อง ทำให้ query ข้อมูง่ายและสนุก แตถ้าอยากหลีกเลี่ยง N+1 queries ต้องระวัง

ถ้า scope เริ่มทำปัญหา ให้ห่อมันใน association และ preload ไม่ได้ทำงานเพิ่มมาก และประหยัด SQL calls มาก