12/08/2018, 17:45

Thủ thuật truy vấn ActiveRecord

Phần yêu thích của tôi trong Rails rõ ràng là scope ActiveRecord. Tính thể hiện (expressiveness) và khả năng tái sử dụng (reusability) của nó thật tuyệt vời. Dưới đây là một vài thủ thuật mà tôi thường sử dụng. Hãy cùng xem chi tiết các thử thuật này: Giả sử chúng ta bảng users với một liên ...

Phần yêu thích của tôi trong Rails rõ ràng là scope ActiveRecord. Tính thể hiện (expressiveness) và khả năng tái sử dụng (reusability) của nó thật tuyệt vời.

Dưới đây là một vài thủ thuật mà tôi thường sử dụng. Hãy cùng xem chi tiết các thử thuật này:

Giả sử chúng ta bảng users với một liên kết tới profile.

Nếu bạn cần truy vấn user nào có profile được xác thực, bạn có thể sẽ làm:

# app/models/user.rb 

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

Dường như ở đây chúng ta có một cách tiếp cận sai lầm. Profile logic hiện tại đang bị xuất hiện trong model User. Điều này trái ngược với nguyên tắc đóng gói theo hướng đổi tượng. Bạn có nhận ra vấn đề này?

Hãy thử với cách này xem sao:

# app/models/profile.rb

scope :activated, ->{ where(activated: true) }
# app/models.user.rb

scope :activated, ->{ joins(:profile).merge(Profile.activated) }

Bằng cách này, chúng ta có thể đảm bảo rằng concerns và logic là tách biệt với nhau.

Hãy cẩn thận với cách các bạn sử dụng joins trong ActiveRecord.

Giả sử như trên chúng ta có như sau:

# app/models/user.rb

class User < ApplicationRecord
  has_one :profile
end

# app/models/profile.rb

class Profile < ApplicationRecord
  has_many :skills
end

# app/models/skill.rb

class Skill < ApplicationRecord
  ...
end

Mặc định nó sẽ sử dụng INNER JOIN tuy nhiên:

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 bạn nên sử dụng:

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

Giả sử bạn cần lấy ra user mà không có post nổi tiếng nào được liên kết và bạn hướng tới truy vấn NOT EXISTS. Bạn có thể viết nó một cách dễ dàng với các phương thức tiện lợi bên dưới.

# app/models/post.rb

class Post < ApplicationRecord
  scope :famous, ->{ where("view_count > ?", 1_000) }
end
# app/models/user.rb

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

Bạn có thể làm một mẫu tương tự với EXITS

Giả sử bạn cần lấy các bài post của một nhóm nhỏ user

Tôi đã thấy rất nhiều developer làm dụng pluck theo cách này

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

Lỗ hổng ở đây là hai truy vấn SQL sẽ được chạy: một truy vấn tìm nạp các id của user, một truy vấn khác để lấy các post từ các user_ids này.

Bạn có thể đạt được kết quả tương tự với một truy vấn chứa truy vấn phụ (subquery):

  Post.where(user_id: User.created_last_month)

Việc này ActiveRecord có thể xử lí thay cho bạn

Đừng quên rằng truy vấn ActiveRecord có thể được nối thêm với .to_sql để tạo chuỗi SQL và bằng .explain để lấy chi tiết, ước tính phức tạp, v.v ...

Tuy nhiên có một số method của ActiveRecord không tương tắc vs to_sql ví dụ như: delete, delete_all ...

Tôi đoán rằng rất nhiều bạn mong muốn khi sử dụng

User.where.not(tall: true)

sẽ được generate thành

=> SELECT users.* FROM users WHERE users.tall <> 't'  # đối với postgres version

Câu truy vấn trên sẽ trả về tất cả user có trường tall được set là false, nhưng không trả về các user có trường tall bị NULL.

Để làm được điều đó chúng ta cần phải điều chỉnh lại một chút như sau:

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

Hoặc là

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

Cảm ơn các bạn đã theo dõi. Bài viết này được dịch từ ActiveRecord query tricks

0