関連付けのカウントがゼロより大きいすべてのレコードを検索します


98

簡単だと思ったことをしようとしていますが、簡単ではないようです。

多くの空席があるプロジェクトモデルがあります。

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

少なくとも1つの空席があるすべてのプロジェクトを取得したいと考えています。私はこのようなものを試しました:

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

しかしそれは言う

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)

回答:


65

joinsはデフォルトで内部結合を使用Project.joins(:vacancies)するので、を使用すると、事実上、空席が関連付けられているプロジェクトのみが返されます。

更新:

コメントの@mackskatzで指摘されているように、group句がない場合、上記のコードは複数の空席があるプロジェクトの重複プロジェクトを返します。重複を削除するには、

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

更新:

@Tolseeで指摘されているように、も使用できますdistinct

Project.joins(:vacancies).distinct

例として

[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
ただし、group by句を適用しないと、複数の空席を持つプロジェクトの複数のProjectオブジェクトが返されます。
mackshkatz 2017年

1
ただし、効率的なSQLステートメントは生成されません。
David Aldridge、

まあそれはあなたのためのレールです。SQLの回答を提供できる場合(そしてこれが効率的でない理由を説明できる場合)、それははるかに役立ちます。
jvnill

どう思いますProject.joins(:vacancies).distinctか?
Tolsee

1
それは@Tolsee btw:Dです
Tolsee

167

1)少なくとも1つの空席があるプロジェクトを取得するには:

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

2)複数の空席があるプロジェクトを取得するには:

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

3)または、Vacancyモデルがカウンターキャッシュを設定する場合:

belongs_to :project, counter_cache: true

その後、これも機能します:

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

の活用ルールは手動で指定vacancyする必要があるかもしれませんんか?


2
これはそうではありませんProject.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')か?プロジェクトIDの代わりに空席の数をクエリする
Keith Mattix 2018

いいえ、@ KeithMattix、そうであってはなりません。それはすることができ、それはあなたに、より良い読み込む場合は、しかし、こと。それは好みの問題です。カウントは、すべての行に値があることが保証されている結合テーブルの任意のフィールドで実行できます。ほとんど意味のある候補であるprojects.idproject_idvacancies.idproject_id結合が行われるフィールドであるため、私は数えることにしました。必要に応じて、結合のスパイン。これは結合テーブルであることも思い出します。
アルタ

36

ええ、vacancies結合のフィールドではありません。私はあなたが望むと信じています:

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

groupor uniqと組み合わせたhas_manyテーブルへの内部結合の実行は、潜在的に非常に非効率的であり、SQLでは、これはEXISTS相関サブクエリで使用する準結合として実装する方が適切です。

これにより、クエリオプティマイザーは、必要なテーブルをプローブして、正しいproject_idを持つ行の存在を確認できます。そのproject_idを持つ行が1つであっても100万であっても関係ありません。

これはRailsでは簡単ではありませんが、次の方法で実現できます。

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

同様に、空室がないすべてのプロジェクトを検索します。

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

編集:Railsの最近のバージョンではexists、arelに委任されることに依存しないように指示する非推奨警告が表示されます。これを修正してください:

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

編集:生のSQLに不快な場合は、以下を試してください。

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

arel_tableたとえば、クラスメソッドを追加しての使用を非表示にすることで、これを煩雑さから解放できます。

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

... そう ...

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

これら2つの提案は機能していないようです...サブクエリVacancy.where("vacancies.project_id = projects.id").exists?trueまたはを生成しますfalseProject.where(true)ですArgumentError
Les Nightingill

Vacancy.where("vacancies.project_id = projects.id").exists?は実行されません– projectsリレーションがクエリに存在しないため(そして上記のサンプルコードにも疑問符がないため)、エラーが発生します。したがって、これを2つの式に分解することは無効であり、機能しません。最近のRails Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)では非推奨の警告が出されます...質問を更新します。
David Aldridge、

4

Rails 4+では、includesまたはeager_loadを使用して同じ答えを取得することもできます。

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

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

4

より簡単な解決策があると思います:

Project.joins(:vacancies).distinct

1
「distinct」を使用することも可能です。例:Project.joins(:vacancies).distinct
Metaphysiker

あなたが正しいです!#uniqの代わりに#distinctを使用することをお勧めします。#uniqはすべてのオブジェクトをメモリにロードしますが、#distinctはデータベース側で計算を行います。
ユーリカルポビッチ

3

Railsの魔法があまりなくても、次のことができます。

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

このタイプの条件は、ほとんどの作業がDB側で直接行われるため、すべてのRailsバージョンで機能します。さらに、チェーン.count方式もうまく機能します。私はProject.joins(:vacancies)以前のようなクエリに火傷しました。もちろん、DBに依存しないため、賛否両論があります。


1
「select count(*)..」サブクエリはプロジェクトごとに実行されるため、これはjoinおよびgroupメソッドよりもはるかに低速です。
YasirAzgar 2018年

@YasirAzgar結合およびグループ化メソッドは、子の行が100万個存在する場合でも、すべての子行にアクセスするため、「既存の」メソッドよりも低速です。
David Aldridge

0

テーブルからすべての列を選択するのEXISTSSELECT 1はなく、を使用することもできvacanciesます。

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

-6

エラーは、基本的に空室はプロジェクトの列ではないことを示しています。

これはうまくいくはずです

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

7
aggregate functions are not allowed in WHERE
Kamil Lelonek 2014年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.