web-dev-qa-db-ja.com

Railsに関連するレコードのないレコードを検索したい

単純な関連付けを検討してください...

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

ARelやmeta_whereに友達がいないすべての人を取得する最もクリーンな方法は何ですか?

そして、has_many:throughバージョンについてはどうですか

class Person
   has_many :contacts
   has_many :friends, :through => :contacts, :uniq => true
end

class Friend
   has_many :contacts
   has_many :people, :through => :contacts, :uniq => true
end

class Contact
   belongs_to :friend
   belongs_to :person
end

私は実際にcounter_cacheを使いたくありません-そして私が読んだものからはhas_manyで動作しません:through

すべてのperson.friendsレコードをプルしてRubyでループしたくありません-meta_search gemで使用できるクエリ/スコープが必要です

クエリのパフォーマンスコストを気にしません

そして、実際のSQLから離れるほど良い...

165
craic.com

これはまだSQLにかなり近いですが、最初のケースでは友人のいない全員を取得する必要があります。

Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
100
Unixmonkey

より良い:

Person.includes(:friends).where( :friends => { :person_id => nil } )

Hmtの場合、基本的に同じことです。友達のいない人にも連絡先がないという事実に依存します。

Person.includes(:contacts).where( :contacts => { :person_id => nil } )

更新

コメントにhas_oneに関する質問があるので、更新してください。ここでのコツは、includes()はアソシエーションの名前を期待しますが、whereはテーブルの名前を期待するということです。 has_oneの場合、関連は通常単数形で表現されるため、変更されますが、where()部分はそのままです。したがって、Personのみhas_one :contactの場合、ステートメントは次のようになります。

Person.includes(:contact).where( :contacts => { :person_id => nil } )

更新2

誰かが逆の、人のいない友人について尋ねました。以下にコメントしたように、これは実際に、最後のフィールド(上記::person_id)が実際に返すモデルに関連する必要はなく、結合のフィールドでなければならないことを実感させました。テーブル。それらはすべてnilになるので、どれでもかまいません。これにより、上記の簡単なソリューションが得られます。

Person.includes(:contacts).where( :contacts => { :id => nil } )

そして、これを切り替えて、人のいない友達を返すことがさらに簡単になり、前のクラスのみを変更します:

Friend.includes(:contacts).where( :contacts => { :id => nil } )

更新3-Rails 5

優れたRails 5ソリューション(@Ansonに以下の回答に+1を与えてください)のおかげで、left_outer_joinsを使用して関連付けのロードを回避できます。

Person.left_outer_joins(:contacts).where( contacts: { id: nil } )

人々がそれを見つけることができるように私はそれをここに含めましたが、彼はこのための+1に値します。素晴らしい追加!

405
smathy

smathyにはRails 3という良い答えがあります。

Rails 5の場合、left_outer_joinsを使用して関連付けのロードを回避できます。

Person.left_outer_joins(:contacts).where( contacts: { id: nil } )

api docs をご覧ください。プルリクエストで導入されました #12071

143
Anson

友達がいない人

Person.includes(:friends).where("friends.person_id IS NULL")

または、少なくとも1人の友人がいる

Person.includes(:friends).where("friends.person_id IS NOT NULL")

Friendにスコープを設定して、Arelでこれを行うことができます

class Friend
  belongs_to :person

  scope :to_somebody, ->{ where arel_table[:person_id].not_eq(nil) }
  scope :to_nobody,   ->{ where arel_table[:person_id].eq(nil) }
end

そして、少なくとも1人の友人がいる人:

Person.includes(:friends).merge(Friend.to_somebody)

友だち:

Person.includes(:friends).merge(Friend.to_nobody)
13
novemberkilo

DmarkowとUnixmonkeyの両方の答えから、必要なものが得られます-ありがとうございます!

私は実際のアプリで両方を試し、それらのタイミングを取得しました-2つのスコープは次のとおりです:

class Person
  has_many :contacts
  has_many :friends, :through => :contacts, :uniq => true
  scope :without_friends_v1, -> { where("(select count(*) from contacts where person_id=people.id) = 0") }
  scope :without_friends_v2, -> { where("id NOT IN (SELECT DISTINCT(person_id) FROM contacts)") }
end

これを実際のアプリで実行しました-700個の 'Person'レコードを持つ小さなテーブル-平均5回の実行

Unixmonkeyのアプローチ(:without_friends_v1)813ms /クエリ

dmarkowのアプローチ(:without_friends_v2)891ms /クエリ(〜10%遅い)

しかし、その後、私はDISTINCT()...への呼び出しを必要としないことに気付きました。NOPersonのないContactsレコードを探しているので、NOT IN連絡先のリストperson_ids。だから私はこのスコープを試しました:

  scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") }

同じ結果が得られますが、平均425ミリ秒/コール-ほぼ半分の時間で...

今、あなたは他の同様のクエリでDISTINCTを必要とするかもしれません-しかし、私の場合、これはうまくいくようです。

ご協力いただきありがとうございます

11
craic.com

残念ながら、SQLを含むソリューションを検討している可能性がありますが、スコープに設定して、そのスコープを使用することはできます。

class Person
  has_many :contacts
  has_many :friends, :through => :contacts, :uniq => true
  scope :without_friends, where("(select count(*) from contacts where person_id=people.id) = 0")
end

次に、それらを取得するには、Person.without_friendsを実行します。また、これを他のArelメソッドとチェーンすることもできます:Person.without_friends.order("name").limit(10)

5
Dylan Markow

NOT EXISTS相関サブクエリは、特に行レコード数と親レコードに対する子レコードの比率が増加するにつれて高速になります。

scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")
1
David Aldridge

また、たとえば1人の友人によって除外するには:

Friend.where.not(id: other_friend.friends.pluck(:id))
1
Dorian