ActiveRecord’s Queries Tricks
Bài đăng này đã không được cập nhật trong 6 năm
Trong quá trình làm việc, bạn luôn muốn làm sao cho code mình viết thật gọn và tối ưu. Đặc biệt là các truy vấn vào cơ sở dữ liệu. Điều này vô cùng quan trọng vì nó ảnh hưởng đến hiệu suất của ứng dụng. Sau đây mình xin giới thiệu 1 vài cách giúp bạn viết scope trong Model 1 cách hiệu quả. Hi vọng bài viết có thể giúp ích cho bạn.
1. Join có điều kiện ở bảng liên kết
Dùng scope trong Active Record là một cách tuyệt vời để bạn viết code mà k cần lặp lại các truy vấn ở khắp mọi nơi..
Nếu có thể sử dụng lại 1 scope từ 1 model khác thì quả là tuyệt vời. Chúng ta sẽ đi vào 1 ví dụ
Chúng ta có 2 model: Customer
và Device
. 1 Customer
có 1 Device
(quan hệ has_one
)/
Trong Device
ta viết 1 scope lấy ra tất cả các thiết bị mà đã được ship:
class Device < ActiveRecord::Base
scope :shipped, -> {
where.not(shipped_at: nil)
}
end
Ở Customer
ta cũng muốn lấy ra tất cả các customer mà có thiết bị đã được ship. Thường thì chúng ta sẽ viết scope như sau
class Customer < ActiveRecord::Base
scope :with_shipped_device, -> {
joins(:device).where.not(device: {shipped_at: nil})
end
end
Và câu truy vấn SQL được tạo ra là:
SELECT `customers`.* FROM `customers`
INNER JOIN `devices` ON `devices`.`customer_id` = `customers`.`id`
WHERE (`devices`.`shipped_at` IS NOT NULL)
Nhược điểm ở đây là:
- Thực hiện 2 scope gần như tương tự nhau
- Ta không tái sử dụng được đoạn scope trong model
Device
- Phải viết đạon SQL khá là dài
Cách khắc phục là ta sử dụng Relation#merge
Đây là một cách đơn giản để tái sử dụng, tránh trùng lặp code. Bạn có thể dùng merge
để kết hợp với scope khác từ model khác. Do vậy scope trong Customer
có thể viết lại được là
class Customer < ActiveRecord::Base
scope :with_shipped_device, -> {
joins(:device).merge(Device.shipped)
}
end
Câu lệnh này trả về đoạn truy vấn SQL tương tự scope bên trên Ưu điểm
- Ngắn gọn
- Truy vấn dùng để select các device chỉ ở 1 nơi, ta có thể tái sử dụng, không bị trùng lặp code
2. Những câu lệnh join lồng nhau
Hãy cẩn thận khi dùng joins trong ActiveRecord, giả sử User
có 1 Profile
và 1 Profile
lại có nhiều Skills
. Mặc định là sử dụng INNER JOIN
nhưng hãy xem ví dụ dưới đây
User.joins(:profiles).merge(Profile.joins(:skills))
=> SELECT users.* FROM users
INNER JOIN profiles ON profiles.user_id = users.id
LEFT OUTER JOIN skills ON skills.profile_id = profiles.id
User.joins(profiles: :skills)
=> SELECT users.* FROM users
INNER JOIN profiles ON profiles.user_id = users.id
INNER JOIN skills ON skills.profile_id = profiles.id
2 câu truy vấn cho ta 1 cái là LEFT OUTER JOIN
còn 1 cái là INNER JOIN
3. Subqueries
Ta muốn lấy tất cả cácPost
mà có user
được tạo bởi tháng trước: Mọi người thường viết:
Post.where(user_id: User.created_last_month.pluck(:id))
Lỗi ở đây là 2 truy vấn SQL sẽ được chạy, 1 cái để lấy list ids của User
, còn 1 cái khác sẽ lấy toàn bộ Post
có user_ids.
Bạn có thể viết câu truy vấn theo 1 cách khác để đạt được kết quả tương tự với 1 subquery:
Post.where(user_id: User.created_last_month)
ActiveRecord sẽ handle nó giúp bạn
4. Explain
Đừng quên với các truy vấn ActiveRecord ta có thể dùng .to_sql
để nhìn chúng dưới dạng câu truy vấn SQL và .explain
để xem thông tin chi tiết, ước tính độ phức tạp...
5. Boolean
Ta viết scope
User.where.not(tall: true)
Để tạo ra câu truy vấn
SELECT users.* FROM users WHERE users.tall <> 't'
Câu truy vấn trên sẽ trả về toàn bộ user có tall
là false
nhưng nó k bao gồm trường hợp tall
là NULL
Bạn nên viết lại thành
User.where("users.tall IS NOT TRUE")
hoặc
User.where(tall: [false, nil])
Hi vọng bài viết có thể giúp ích 1 phần nào đó khi bạn viết scope trong Model.
Nguồn Tham khảo:
https://medium.com/rubyinside/active-records-queries-tricks-2546181a98dd http://aokolish.me/blog/2015/05/26/how-to-simplify-active-record-scopes-that-reference-other-tables/
All rights reserved