Các thủ thuật khi dùng Active Record

Việc viết code trong sáng và tái sử dụng là điều mà tất cả các lập trình viên nên làm. Đối với việc sử dụng Active Record cũng vậy, mình sẽ hướng dẫn các bạn một 5 trick phổ biến để viết code dễ hiểu và trong sáng hơn.

1. Sử dụng câu lệnh truy vấn có điều kiện với bảng liên kết

Ở ví dụ dưới đây mình có một bảng Users quan hệ với profile. Nếu bạn cần liên kết với bảng profile với điều kiện nào đó thì bạn có thể sẽ sử dụng đoạn code sau:

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

Tuy nhiên mình cảm thấy cách làm trên có vẻ không ổn, vì sao? Bởi vì các logic của model Profile giờ lại nằm ở trong model User, điều này hoàn toàn sai lệch với tính đóng gói trong lập trình hướng đối tượng. Vì vậy mình sẽ sửa chúng như sau:

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

Với cách code trên, mình có thể đảm bảo được tính riêng tư mỗi model và đáp ứng được đúng logic của bài toán.

2. Lưu ý khi sử dụng các truy vấn lồng

Việc sử dụng joins trong ActiveRecord cũng có một số điểm mà các bạn nên lưu ý, ví dụ bảng User quan hệ 1-1 với Profile, bảng Profile quan hệ 1-n với bảng Skills. Mặc định ở đây sẽ sử dụng INNER JOIN.

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

cách dùng trên cùng ổn tuy nhiên cũng sử dụng joins nhưng vừa có INNER JOIN VÀ LEFT OUTER JOIN, vì vậy mình sẽ dùng đoạn code sau:

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

3. Exist query

Ví dụ bạn muốn lấy danh sách user không có các bài viết đứng top các bài viết hay và các bạn sẽ nghĩ đến việc sử dụng câu lệnh NOT EXISTS. Bạn có thể viết đoạn code cho ví dụ trên một cách tốt nhất và trong sáng nhất như cách 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ũng có thể làm tương tự đối với các trường hợp sử dụng câu lệnh EXISTS.

4. Subqueries

Giả sử bạn cần lấy tất cả user đã viết bài từ tháng trước và thường sẽ sử dụng pluck cho trường hợp này:

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

Tuy nhiên, khi chạy đoạn code trên sẽ có 2 câu lệnh SQL được thực thi: đầu tiên nó sẽ tìm các id của người dùng, sau đó nó sẽ lấy các bài viết dựa vài các id này. Việc sử dụng nhiều câu lệnh sql sẽ khiến hệ thống chạy chậm hơn và đó là điều không nên. Vì vậy bạn có thể đạt được kết quả tương tự với một truy vấn có truy vấn con như sau:

Post.where(user_id: User.created_last_month)

ActiveRecord sẽ xử lí nó cho bạn.

5. Một số điều cơ bản khi sử dụng ActiveRecord

Đừng quên rằng các câu lệnh ActiveRecord có thể được kết hợp với .to_sql để tạo chuỗi SQL và dùng .explain để biết chi tiết câu lệnh, ước tính độ phức tạp của câu lênh, v.v...

Tài liệu tham khảo