Viblo Learning
0

ActiveRecord’s Queries Tricks

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: CustomerDevice. 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à:

  1. Thực hiện 2 scope gần như tương tự nhau
  2. Ta không tái sử dụng được đoạn scope trong model Device
  3. 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

  1. Ngắn gọn
  2. 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ó tallfalse nhưng nó k bao gồm trường hợp tallNULL 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

Viblo
Let's register a Viblo Account to get more interesting posts.