+3

Một số mẹo viết câu truy vấn hiệu quả (tiếp)

Ở bài trước, mình có giới thiệu một số cách để tối ưu hóa câu truy vấn ( link bài trước ) Ở bài này mình giới thiệu thêm 1 số mẹo nhỏ mà mọi người thường ít để ý.

Viết câu query sử dụng điều kiện trong bảng liên kết

Bạn có một bảng User và bảng Profile có liên kết với bảng User Nếu bạn muốn viết một câu truy vấn lấy ra những UsersProfile đã được validated, thường thì sẽ viết thế này:

# User model
scope :activated, ->{
  joins(:profile).where(profiles: { activated: true })
}

Cách này tuy vẫn trả ra kết quả, nhưng nó là hướng tiếp cận sai, logic của Profile bị lộ bên trong model Users, điều này vi phạm tính đóng gói của lập trình hướng đối tượng. Ta nên làm theo cách này:

# Profile model
scope :activated, ->{ where(activated: true) }
# User model
scope :activated, ->{ joins(:profile).merge(Profile.activated) }

Sự khác biệt của nested joins

Hãy cẩn thận với cách bạn sử dụng joins trong ActiveRecord, ví dụ bảng Users có mối quan hệ 1 -1 với bảng Profile, bảng Profile có mối quan hệ 1 - n với bảng Skill, mặc định là nó sẽ sử dụng INNER JOIN nhưng...

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

Vì vậy nên sử dụng cách này hơn:

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

Exist query

Khi bạn muốn lấy ra những Users mà không có những bài viết nổi tiếng, tức là bạn đang hướng đến câu truy vấn NOT EXISTS, bạn có thể viết nó vô cùng dễ dàng với những phương thức tiện lợi dưới đây

# Post
scope :famous, ->{ where("view_count > ?", 1_000) }
# User
scope :without_famous_post, ->{
  where(_not_exists(Post.where("posts.user_id = users.id").famous))
}
def self._not_exists(scope)
  "NOT #{_exists(scope)}"
end
def self._exists(scope)
  "EXISTS(#{scope.to_sql})"
end

Bạn có thể làm tương tự với EXISTS

Subqueries

Giả sử bạn cần lấy những bài viết của một tập hợp Users nhất định, nhiều người thường lạm dụng pluck thế này:

Post.where(user_id: User.created_last_month.pluck(:id))

Lỗi ở đây là nó sẽ thực hiện 2 câu truy vấn: một câu để lấy ra id của Users, một câu để lấy ra những bài viết của users_id đấy. Chúng ta hoàn toàn có thể lấy được kết quả tương tự với một câu query có chứa subquery:

Post.where(user_id: User.created_last_month)

ActiveRecord đã làm tất cả cho bạn.

Quay trở về dạng đơn giản

Đừng quên rằng câu truy vấn ActiveRecord có thể kết hợp với .to_sql để hiển thị ra một câu lệnh SQL và .explain để lấy chi tiết,...

Booleans

Tôi đoán bạn kì vọng User.where.not(tall: true) sẽ tương tự SELECT users.* FROM users WHERE users.tall <> 't' Nhưng thực tế là nó sẽ trả về Users có tail được set là false, còn sẽ không trả về tail được set là NULL Sẽ tốt hơn nếu viết thế này: User.where("users.tall IS NOT TRUE") hoặc User.where(tall: [false, nil])

Nguồn tham khảo: https://medium.com/@apneadiving/active-records-queries-tricks-2546181a98dd


All Rights Reserved

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