Tìm tất cả các bản ghi có số lượng liên kết lớn hơn 0


98

Tôi đang cố gắng làm một việc mà tôi nghĩ nó sẽ đơn giản nhưng có vẻ như không phải vậy.

Tôi có một mô hình dự án còn nhiều vị trí tuyển dụng.

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

Tôi muốn nhận tất cả các dự án có ít nhất 1 chỗ trống. Tôi đã thử một cái gì đó như thế này:

Project.joins(:vacancies).where('count(vacancies) > 0')

nhưng nó nói

SQLite3::SQLException: no such column: vacancies: SELECT "projects".* FROM "projects" INNER JOIN "vacancies" ON "vacancies"."project_id" = "projects"."id" WHERE ("projects"."deleted_at" IS NULL) AND (count(vacancies) > 0).

Câu trả lời:


65

joinssử dụng liên kết bên trong theo mặc định vì vậy việc sử dụng Project.joins(:vacancies)sẽ chỉ trả về các dự án có vị trí trống liên quan.

CẬP NHẬT:

Như đã chỉ ra bởi @mackskatz trong nhận xét, nếu không có groupđiều khoản, đoạn mã trên sẽ trả về các dự án trùng lặp cho các dự án có nhiều hơn một vị trí tuyển dụng. Để loại bỏ các bản sao, hãy sử dụng

Project.joins(:vacancies).group('projects.id')

CẬP NHẬT:

Như đã chỉ ra bởi @Tolsee, bạn cũng có thể sử dụng distinct.

Project.joins(:vacancies).distinct

Như một ví dụ

[10] pry(main)> Comment.distinct.pluck :article_id
=> [43, 34, 45, 55, 17, 19, 1, 3, 4, 18, 44, 5, 13, 22, 16, 6, 53]
[11] pry(main)> _.size
=> 17
[12] pry(main)> Article.joins(:comments).size
=> 45
[13] pry(main)> Article.joins(:comments).distinct.size
=> 17
[14] pry(main)> Article.joins(:comments).distinct.to_sql
=> "SELECT DISTINCT \"articles\".* FROM \"articles\" INNER JOIN \"comments\" ON \"comments\".\"article_id\" = \"articles\".\"id\""

1
Tuy nhiên, nếu không áp dụng nhóm theo mệnh đề, điều này sẽ trả về nhiều đối tượng Dự án cho các Dự án có nhiều hơn một Vị trí tuyển dụng.
mackshkatz

1
Tuy nhiên, không tạo ra một câu lệnh SQL hiệu quả.
David Aldridge

Đó là Rails dành cho bạn. Nếu bạn có thể cung cấp câu trả lời sql (và giải thích lý do tại sao điều này không hiệu quả), điều đó có thể hữu ích hơn rất nhiều.
'19

Bạn nghĩ sao về những gì Project.joins(:vacancies).distinct?
Tolsee

1
Đó là @Tolsee btw: D
Tolsee

167

1) Nhận Dự án có ít nhất 1 vị trí tuyển dụng:

Project.joins(:vacancies).group('projects.id')

2) Để nhận các Dự án có nhiều hơn 1 vị trí tuyển dụng:

Project.joins(:vacancies).group('projects.id').having('count(project_id) > 1')

3) Hoặc, nếu Vacancymô hình đặt bộ đệm truy cập:

belongs_to :project, counter_cache: true

thì điều này cũng sẽ hoạt động:

Project.where('vacancies_count > ?', 1)

Quy tắc uốn cho vacancycó thể cần được chỉ định thủ công ?


2
Điều này không nên Project.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')? Truy vấn số lượng chỗ trống thay cho id dự án
Keith Mattix

Không, @KeithMattix, không nên như vậy. Nó có thể được, tuy nhiên, nếu nó đọc tốt hơn đối với bạn; đó là một vấn đề của sở thích. Việc đếm có thể được thực hiện với bất kỳ trường nào trong bảng nối được đảm bảo có giá trị trong mọi hàng. Hầu hết các thí sinh có ý nghĩa là projects.id, project_id, và vacancies.id. Tôi đã chọn đếm project_idvì nó là trường mà phép nối được thực hiện; cột sống của tham gia nếu bạn muốn. Nó cũng nhắc nhở tôi rằng đây là một bảng tham gia.
Arta

36

Yeah, vacancieskhông phải là một lĩnh vực trong tham gia. Tôi tin rằng bạn muốn:

Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")

16
# None
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 0')
# Any
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 0')
# One
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 1')
# More than 1
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 1')

5

Việc thực hiện một phép nối bên trong đến bảng has_many kết hợp với dấu grouphoặc uniqcó khả năng rất kém hiệu quả và trong SQL, điều này sẽ được triển khai tốt hơn dưới dạng một phép nối bán phần sử dụng EXISTSvới một truy vấn con tương quan.

Điều này cho phép trình tối ưu hóa truy vấn thăm dò bảng vị trí trống để kiểm tra sự tồn tại của một hàng có đúng project_id. Không quan trọng là có một hàng hay một triệu hàng có project_id đó.

Điều đó không đơn giản như trong Rails, nhưng có thể đạt được bằng:

Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)

Tương tự, hãy tìm tất cả các dự án không có vị trí tuyển dụng:

Project.where.not(Vacancies.where("vacancies.project_id = projects.id").exists)

Chỉnh sửa: trong các phiên bản Rails gần đây, bạn nhận được cảnh báo không dùng nữa cho biết bạn không nên tin tưởng vào existsviệc được ủy quyền cho arel. Khắc phục điều này bằng:

Project.where.not(Vacancies.where("vacancies.project_id = projects.id").arel.exists)

Chỉnh sửa: nếu bạn không thoải mái với SQL thô, hãy thử:

Project.where.not(Vacancies.where(Vacancy.arel_table[:project_id].eq(Project.arel_table[:id])).arel.exists)

Bạn có thể làm cho điều này bớt lộn xộn hơn bằng cách thêm các phương thức lớp để ẩn việc sử dụng arel_table, ví dụ:

class Project
  def self.id_column
    arel_table[:id]
  end
end

... vì thế ...

Project.where.not(
  Vacancies.where(
    Vacancy.project_id_column.eq(Project.id_column)
  ).arel.exists
)

hai gợi ý này dường như không hoạt động ... truy vấn con Vacancy.where("vacancies.project_id = projects.id").exists?mang lại kết quả truehoặc false. Project.where(true)là một ArgumentError.
Les Nightingill

Vacancy.where("vacancies.project_id = projects.id").exists?sẽ không thực thi - nó sẽ gây ra lỗi vì projectsquan hệ sẽ không tồn tại trong truy vấn (và không có dấu chấm hỏi trong mã mẫu ở trên). Vì vậy, việc phân tách điều này thành hai biểu thức không hợp lệ và không hoạt động. Trong Rails gần đây Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)đưa ra cảnh báo không dùng nữa ... Tôi sẽ cập nhật câu hỏi.
David Aldridge

4

Trong Rails 4+, bạn cũng có thể sử dụng bao gồm hoặc háo hức để nhận được câu trả lời tương tự:

Project.includes(:vacancies).references(:vacancies).
        where.not(vacancies: {id: nil})

Project.eager_load(:vacancies).where.not(vacancies: {id: nil})

4

Tôi nghĩ có một giải pháp đơn giản hơn:

Project.joins(:vacancies).distinct

1
Cũng có thể sử dụng "khác biệt", ví dụ như Project.joins (: tuyển dụng) .distinct
Metaphysiker

Bạn đúng rồi! Tốt hơn nên sử dụng #distinction thay vì #uniq. #uniq sẽ tải tất cả các đối tượng vào bộ nhớ, nhưng #distinction sẽ thực hiện các phép tính trên một phía cơ sở dữ liệu.
Yuri Karpovich

3

Không cần nhiều phép thuật Rails, bạn có thể làm:

Project.where('(SELECT COUNT(*) FROM vacancies WHERE vacancies.project_id = projects.id) > 0')

Loại điều kiện này sẽ hoạt động trong tất cả các phiên bản Rails vì phần lớn công việc được thực hiện trực tiếp trên phía DB. Ngoài ra, .countphương pháp chuỗi cũng sẽ hoạt động tốt. Tôi đã bị đốt cháy bởi các truy vấn như Project.joins(:vacancies)trước đây. Tất nhiên, có những ưu và khuyết điểm vì nó không phải là DB bất khả tri.


1
Điều này chậm hơn nhiều so với phương thức tham gia và nhóm, vì truy vấn con 'select count (*) ..' sẽ thực thi cho mỗi dự án.
YasirAzgar

@YasirAzgar Phương thức nối và nhóm chậm hơn phương thức "tồn tại" vì nó sẽ vẫn truy cập tất cả các hàng con, ngay cả khi có hàng triệu hàng trong số đó.
David Aldridge

0

Bạn cũng có thể sử dụng EXISTSvới SELECT 1thay vì chọn tất cả các cột từ vacanciesbảng:

Project.where("EXISTS(SELECT 1 from vacancies where projects.id = vacancies.project_id)")

-6

Lỗi nói với bạn rằng vị trí tuyển dụng không phải là một cột trong các dự án, về cơ bản.

Điều này sẽ hoạt động

Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')

7
aggregate functions are not allowed in WHERE
Kamil Lelonek
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.