Temukan semua rekaman yang memiliki jumlah pengaitan lebih besar dari nol

98

Saya mencoba melakukan sesuatu yang menurut saya sederhana, tetapi ternyata tidak.

Saya memiliki model proyek yang memiliki banyak lowongan.

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

Saya ingin mendapatkan semua proyek yang memiliki minimal 1 lowongan. Saya mencoba sesuatu seperti ini:

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

tapi tertulis

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).

jphorta.dll
sumber

Jawaban:

65

joinsmenggunakan gabungan dalam secara default sehingga penggunaan Project.joins(:vacancies)hanya akan mengembalikan proyek yang memiliki lowongan terkait.

MEMPERBARUI:

Seperti yang ditunjukkan oleh @mackskatz di komentar, tanpa groupklausa, kode di atas akan mengembalikan proyek duplikat untuk proyek dengan lebih dari satu lowongan. Untuk menghapus duplikat, gunakan

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

MEMPERBARUI:

Seperti yang ditunjukkan oleh @Tolsee, Anda juga bisa menggunakan distinct.

Project.joins(:vacancies).distinct

Sebagai contoh

[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\""
jvnill.dll
sumber
1
Namun, tanpa menerapkan grup dengan klausul ini akan mengembalikan beberapa objek Proyek untuk Proyek yang memiliki lebih dari satu Lowongan.
mackshkatz
1
Tidak menghasilkan pernyataan SQL yang efisien.
David Aldridge
Nah, itulah Rails untuk Anda. Jika Anda dapat memberikan jawaban sql (dan menjelaskan mengapa ini tidak efisien), itu mungkin jauh lebih membantu.
jvnill
Apa pendapat Anda tentang Project.joins(:vacancies).distinct?
Tolsee
1
Ini @Tolsee btw: D
Tolsee
167

1) Untuk mendapatkan Proyek dengan setidaknya 1 lowongan:

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

2) Untuk mendapatkan Proyek dengan lebih dari 1 lowongan:

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

3) Atau, jika Vacancymodel menyetel cache penghitung:

belongs_to :project, counter_cache: true

maka ini juga akan berhasil:

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

Aturan inflasi untuk vacancy mungkin perlu ditentukan secara manual ?

Arta
sumber
2
Bukankah seharusnya demikian Project.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')? Menanyakan jumlah lowongan alih-alih id proyek
Keith Mattix
Tidak, @KeithMattix, seharusnya tidak. Akan tetapi, bisa jadi jika itu lebih baik bagi Anda; ini masalah preferensi. Penghitungan dapat dilakukan dengan bidang apa pun di tabel gabungan yang dijamin memiliki nilai di setiap baris. Kebanyakan calon bermakna adalah projects.id, project_id, dan vacancies.id. Saya memilih untuk menghitung project_idkarena ini adalah bidang tempat gabungan dibuat; tulang belakang bergabung jika Anda mau. Itu juga mengingatkan saya bahwa ini adalah tabel gabungan.
Arta
36

Ya, vacanciesbukan bidang yang di gabung. Saya yakin Anda ingin:

Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")
Peter Alfvin
sumber
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')
Dorian
sumber
5

Melakukan gabungan dalam ke tabel has_many yang dikombinasikan dengan groupatau uniqberpotensi sangat tidak efisien, dan dalam SQL ini akan lebih baik diimplementasikan sebagai gabungan semi yang menggunakan EXISTSdengan subkueri berkorelasi.

Ini memungkinkan pengoptimal kueri untuk menyelidiki tabel lowongan untuk memeriksa keberadaan baris dengan project_id yang benar. Tidak masalah apakah ada satu baris atau sejuta yang memiliki project_id tersebut.

Itu tidak semudah di Rails, tetapi bisa dicapai dengan:

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

Demikian pula, temukan semua proyek yang tidak memiliki lowongan:

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

Sunting: dalam versi Rails terbaru Anda mendapatkan peringatan penghentian yang memberitahu Anda untuk tidak mengandalkan existspendelegasian ke arel. Perbaiki ini dengan:

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

Sunting: jika Anda tidak nyaman dengan SQL mentah, coba:

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

Anda dapat mengurangi kekacauan ini dengan menambahkan metode kelas untuk menyembunyikan penggunaannya arel_table, misalnya:

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

... jadi ...

Project.where.not(
  Vacancies.where(
    Vacancy.project_id_column.eq(Project.id_column)
  ).arel.exists
)
David Aldridge
sumber
kedua saran ini tampaknya tidak berhasil ... subquery Vacancy.where("vacancies.project_id = projects.id").exists?menghasilkan trueatau false. Project.where(true)adalah sebuah ArgumentError.
Les Nightingill
Vacancy.where("vacancies.project_id = projects.id").exists?tidak akan dieksekusi - ini akan menimbulkan kesalahan karena projectsrelasinya tidak akan ada dalam kueri (dan juga tidak ada tanda tanya dalam kode contoh di atas). Jadi menguraikan ini menjadi dua ekspresi tidak valid dan tidak berfungsi. Baru-baru ini, Rails Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)memunculkan peringatan penghentian ... Saya akan memperbarui pertanyaannya.
David Aldridge
4

Di Rails 4+, Anda juga dapat menggunakan include atau eager_load untuk mendapatkan jawaban yang sama:

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

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

Saya pikir ada solusi yang lebih sederhana:

Project.joins(:vacancies).distinct
Yuri Karpovich
sumber
1
Ini juga memungkinkan untuk menggunakan "berbeda", misalnya Project.joins (: vacancies) .distinct
Metaphysiker
Kamu benar! Lebih baik menggunakan #distinct daripada #uniq. #uniq akan memuat semua objek ke dalam memori, tetapi #distinct akan melakukan kalkulasi di sisi database.
Yuri Karpovich
3

Tanpa banyak keajaiban Rails, Anda dapat melakukan:

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

Jenis kondisi ini akan bekerja di semua versi Rails karena sebagian besar pekerjaan dilakukan langsung di sisi DB. Plus, .countmetode rantai juga akan bekerja dengan baik. Saya telah dibakar oleh pertanyaan seperti Project.joins(:vacancies)sebelumnya. Tentu saja ada pro dan kontra karena ini bukan DB agnostik.

konyak
sumber
1
Ini jauh lebih lambat daripada metode bergabung dan grup, karena subkueri 'select count (*) ..' akan dieksekusi untuk setiap project.
YasirAzgar
@YasirAzgar Metode bergabung dan grup lebih lambat daripada metode "ada" karena masih akan mengakses semua baris turunan, meskipun ada jutaan baris.
David Aldridge
0

Anda juga dapat menggunakan EXISTSdengan SELECT 1daripada memilih semua kolom dari vacanciestabel:

Project.where("EXISTS(SELECT 1 from vacancies where projects.id = vacancies.project_id)")
KM Rakibul Islam
sumber
-6

Kesalahannya memberi tahu Anda bahwa lowongan bukanlah kolom dalam proyek, pada dasarnya.

Ini seharusnya berhasil

Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')
wkhatch.dll
sumber
7
aggregate functions are not allowed in WHERE
Kamil Lelonek