Truy vấn con trong activerecord


82

Với SQL, tôi có thể dễ dàng thực hiện các truy vấn phụ như thế này

User.where(:id => Account.where(..).select(:user_id))

Điều này tạo ra:

SELECT * FROM users WHERE id IN (SELECT user_id FROM accounts WHERE ..)

Làm cách nào để thực hiện việc này bằng cách sử dụng 3 activerecord / arel / meta_where của rails?

Tôi cần / muốn truy vấn con thực sự, không có cách giải quyết thay thế bằng ruby ​​(sử dụng một số truy vấn).

Câu trả lời:


125

Rails hiện làm điều này theo mặc định :)

Message.where(user_id: Profile.select("user_id").where(gender: 'm'))

sẽ tạo ra SQL sau

SELECT "messages".* FROM "messages" WHERE "messages"."user_id" IN (SELECT user_id FROM "profiles" WHERE "profiles"."gender" = 'm')

(số phiên bản mà "bây giờ" đề cập đến rất có thể là 3.2)


6
Làm thế nào để làm tương tự nếu điều kiện KHÔNG VÀO?
coorasse

13
@coorasse: Nếu bạn đang sử dụng Rails 4, bây giờ có một notđiều kiện . Tôi đã có thể thực hiện nó trong Rails 3 bằng cách điều chỉnh cách tiếp cận trong bài đăng này : subquery = Profile.select("user_id").where(gender: 'm')).to_sql; Message.where('user_id NOT IN (#{subquery})) Về cơ bản, ActiveRecordcác phương thức được sử dụng để tạo truy vấn con đã hoàn thành, được trích dẫn đúng cách, sau đó được đưa vào truy vấn bên ngoài. Nhược điểm chính là các tham số truy vấn con không bị ràng buộc.
twelve17

3
Chỉ để kết thúc điểm của @elve17 về Rails 4, cú pháp not cụ thể là Message.where.not(user_id: Profile.select("user_id").where(gender: 'm'))- tạo ra một lựa chọn con "NOT IN". Chỉ cần giải quyết vấn đề của tôi ..
Steve Midgley

1
@ChristopherLindblom Khi bạn nói Rails "bây giờ" thực hiện điều này theo mặc định, ý bạn chính xác là gì? Kể từ Rails 3.2? Sẽ thật tuyệt nếu chúng ta có thể thay đổi câu trả lời thành "Rails thực hiện điều này theo mặc định kể từ phiên bản X".
Jason Swett

@JasonSwett Tôi xin lỗi tôi không biết, nó có thể là 3.2 như bạn nói vì nó là phiên bản hiện tại và chỉ chạy các phiên bản đã phát hành. Sẽ suy nghĩ về việc kiểm chứng trong tương lai về phía trước, cảm ơn bạn đã chỉ ra điều này.
Christopher Lindblom

43

Trong ARel, các where()phương thức có thể nhận mảng làm đối số sẽ tạo truy vấn "WHERE id IN ...". Vì vậy, những gì bạn đã viết là dọc theo dòng bên phải.

Ví dụ: mã ARel sau:

User.where(:id => Order.where(:user_id => 5)).to_sql

... tương đương với:

User.where(:id => [5, 1, 2, 3]).to_sql

... sẽ xuất ra SQL sau trên cơ sở dữ liệu PostgreSQL:

SELECT "users".* FROM "users" WHERE "users"."id" IN (5, 1, 2, 3)" 

Cập nhật: để trả lời các bình luận

Được rồi, vì vậy tôi đã hiểu sai câu hỏi. Tôi tin rằng bạn muốn truy vấn con liệt kê rõ ràng các tên cột sẽ được chọn để không đánh vào cơ sở dữ liệu với hai truy vấn (đó là những gì ActiveRecord làm trong trường hợp đơn giản nhất).

Bạn có thể sử dụng projectcho phần selectlựa chọn phụ của mình:

accounts = Account.arel_table
User.where(:id => accounts.project(:user_id).where(accounts[:user_id].not_eq(6)))

... sẽ tạo ra SQL sau:

SELECT "users".* FROM "users" WHERE "users"."id" IN (SELECT user_id FROM "accounts" WHERE "accounts"."user_id" != 6)

Tôi chân thành hy vọng rằng tôi đã cho bạn những gì bạn muốn lần này!


Có, nhưng đây chính xác là điều tôi không muốn vì nó tạo ra hai truy vấn riêng biệt chứ không phải một truy vấn duy nhất có chứa một truy vấn con.
gucki

Xin lỗi vì đã hiểu sai câu hỏi của bạn. Bạn có thể cho một ví dụ về những gì bạn muốn SQL của mình trông như thế nào không?
Scott

Không vấn đề gì. Nó đã được đề cập ở trên: CHỌN * TỪ người dùng WHERE id IN (CHỌN user_id TỪ tài khoản WHERE ..)
gucki

1
À được rồi. Tôi hiểu những gì bạn đang nói bây giờ. Tôi hiểu ý bạn về 2 truy vấn được tạo. May mắn thay, tôi biết cách khắc phục sự cố của bạn! (xem câu trả lời đã sửa đổi)
Scott

23

Tôi đang tự mình tìm kiếm câu trả lời cho câu hỏi này và tôi đã nghĩ ra một cách tiếp cận khác. Tôi chỉ nghĩ rằng tôi sẽ chia sẻ nó - hy vọng nó sẽ giúp ích cho ai đó! :)

# 1. Build you subquery with AREL.
subquery = Account.where(...).select(:id)
# 2. Use the AREL object in your query by converting it into a SQL string
query = User.where("users.account_id IN (#{subquery.to_sql})")

Chơi lô tô! Bango!

Làm việc với Rails 3.1


4
nó thực hiện truy vấn đầu tiên hai lần. tốt hơn nên làm subquery = Account.where(...).select(:id).to_sql query = User.where("users.account_id IN (#{subquery})")
coorasse

9
Nó sẽ chỉ thực hiện truy vấn đầu tiên hai lần trong REPL của bạn vì nó gọi to_s trên truy vấn để hiển thị nó. Nó sẽ chỉ thực thi nó một lần trong ứng dụng của bạn.
Ritchie

Điều gì sẽ xảy ra nếu chúng ta muốn nhiều cột từ các bảng tài khoản?
Ahmad hamza

0

Một thay thế khác:

Message.where(user: User.joins(:profile).where(profile: { gender: 'm' })

0

Đây là ví dụ về truy vấn con lồng nhau bằng cách sử dụng ActiveRecord rails và sử dụng JOIN, nơi bạn có thể thêm các mệnh đề vào mỗi truy vấn cũng như kết quả:

Bạn có thể thêm phạm vi inner_query và ngoài_query lồng nhau vào tệp Mô hình của mình và sử dụng ...

  inner_query = Account.inner_query(params)
  result = User.outer_query(params).joins("(#{inner_query.to_sql}) alias ON users.id=accounts.id")
   .group("alias.grouping_var, alias.grouping_var2 ...")
   .order("...")

Một ví dụ về phạm vi:

   scope :inner_query , -> (ids) {
    select("...")
    .joins("left join users on users.id = accounts.id")
    .where("users.account_id IN (?)", ids)
    .group("...")
   }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.