ActiveRecord’s queries tricks

Khi làm việc với Rails chúng ta có thể thấy một trong những ưu điểm nổi bật của framework này đó chính là các ActiveRecord scope rất clear. Các scope có thể được connect hay sử dụng lại một cách rất dễ dàng. Bài viết dưới đây mình sẽ chia sẻ cho các bạn một vài tricks nhỏ khi làm việc với ActiveRecord model.

I. Join query with condition on the associated table

Giả sử ta có users table cùng với liên kết profile, bài toán được đặt ra là chúng ta cần query những users có profile được activated. Bài toán khá là đơn giản khi ta có thể nghĩ ngay đến giải pháp sau:

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

Tuy nhiên khi soi kĩ đoạn code trên có thể thấy Profile logic lại bị rò rỉ trong User model, điều này trái ngược với nguyên lý đóng gói trong lập trình hướng đối tượng. Vì thế ta có thể tìm kiếm một phương pháp mới vừa chặt chẽ lại rõ ràng hơn như sau:

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

Như vậy các logic đã được viết một cách độc lập, chặt chẽ và dễ dàng kế thừa về sau.

II. Different nested joins

Việc join nhiều bảng nested với nhau quả thật rất đơn giản, ngắn gọn với ActiveRecord ta có thể so sánh 2 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
# Đơn giản 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

III. Exist query

Nếu như muốn truy xuất những người dùng với những bài viết ít nổi bật ta có thể nghĩ ngay đến NOT EXIST query.

# 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

Ta có thể làm tương tự với EXIST.

VI. Subqueries

Ta có thể lấy ví dụ khi muốn lấy ra những bài viết được tạo bởi một nhóm người dùng nào đấy, đa số các dev mới sẽ viết như này:

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

Nhưng nếu check log dòng code trên đã thực hiện 2 câu truy vấn SQL một cách không cần thiết, điều này đã gián tiếp làm giảm performance của ứng dụng. Một query để get ids của user và một query khác để lấy ra những bài viết từ các user_ids này.

Thực tế ta không cần đến 2 query để có một kết quả tương tự

Post.where(user_id: User.created_last_month)

V. Back to basics

Cơ bản AcitveRecord query đều là các query sql string. Vì thế việc áp dụng ActiveRecord đơn giả cũng chỉ là vận dụng kiến thực về truy vấn sql mà thôi 😄.

VI. Booleans

Điều chúng ta cần chú ý ở đâu là một số query ở dạng sau User.where.not(tall: true) SELECT users.* FROM users WHERE users.tall <> 't' (postgres version). Câu lệnh trên trả về các bản ghi mà tall được set là false mà sẽ thiếu đi các bản ghi mà tall chưa được set giá trị hay nói cách khác là giá trị null

Ta có thể thỏa mãn điều kiện trên với cách viết sau

User.where("users.tall IS NOT TRUE")

hay

User.where(tall: [false, nil])

Bài viết trên đây mình đã chia sẻ cho các bạn một số trick khi làm việc với ActiveRecord hy vọng có thể giúp ích cho mọi người trong quá trình làm việc.

Nguồn tham khảo: ActiveRecord’s queries tricks