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


178

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

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から離れているほど良い...

回答:


110

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

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

6
フレンドテーブルに10000000レコードあると想像してみてください。その場合のパフォーマンスはどうですか?
goodniceweb 2016

@goodniceweb重複する頻度によっては、おそらくを削除できますDISTINCT。それ以外の場合、その場合はデータとインデックスを正規化する必要があると思います。私は、friend_idshstoreまたはシリアライズされた列を作成することによってそれを行うかもしれません。その後、あなたは言うことができますPerson.where(friend_ids: nil)
Unixmonkey

SQLを使用する場合は、おそらく使用する方が良いでしょうnot exists (select person_id from friends where person_id = person.id)(または、テーブルが何であるpeople.idpersons.idに応じて、おそらくまたは。) ActiveRecordを使用しようとしていませんでした。
18年

442

より良い:

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するに値します。素晴らしい追加!

アップデート4-Rails 6.1

次の6.1でこれを実行できることを指摘してくれたTim Parkに感謝します。

Person.where.missing(:contacts)

彼がリンクした投稿にも感謝します。


4
これをよりクリーンなスコープに組み込むことができます。
2012年

3
はるかに良い答えですが、なぜ他の1つが受け入れられたと評価されるのかはわかりません。
Tamik Soziev 2013

5
はい、あります。has_one関連付けの名前が1つしかない場合は、includes呼び出しで関連付けの名前を変更する必要があります。だから、それがあったと仮定するとhas_one :contact内部Personその後、あなたのコードは次のようになりますPerson.includes(:contact).where( :contacts => { :person_id => nil } )
smathy

3
Friendモデル(self.table_name = "custom_friends_table_name")でカスタムテーブル名を使用している場合は、を使用しますPerson.includes(:friends).where(:custom_friends_table_name => {:id => nil})
Zek

5
@smathy Rails 6.1のすばらしいアップデートでは、これをmissing正確に行うメソッドが追加されています!
Tim Park

172

smathyはRails 3の良い答えを持っています。

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

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

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


これには欠点がありますか?私がチェックしたところ、.includesより0.1 ms速くロードされました
Qwertie

アソシエーションをロードしないことは、後で実際にアクセスする場合はマイナス面ですが、アクセスしない場合はメリットがあります。私のサイトでは、0.1ミリ秒のヒットはごくわずか.includesなので、読み込み時間の追加コストは、最適化について心配することにはなりません。ユースケースは異なる場合があります。
Anson、

1
Rails 5をまだお持ちでない場合は、これを行うことができますPerson.joins('LEFT JOIN contacts ON contacts.person_id = persons.id').where('contacts.id IS NULL')。スコープとしても問題なく機能します。私はこれをいつもRailsプロジェクトで行っています。
フランク

3
この方法の大きな利点は、メモリの節約です。を実行するとincludes、それらすべてのARオブジェクトがメモリに読み込まれます。これは、テーブルがどんどん大きくなると悪いことになる可能性があります。連絡先レコードにアクセスする必要left_outer_joinsがない場合、は連絡先をメモリにロードしません。SQLリクエストの速度は同じですが、アプリ全体のメリットははるかに大きくなります。
chrismanderson 2017年

2
これは本当にいいです!ありがとう!レールの神々がそれを単純なものとして実装できる場合、Person.where(contacts: nil)またはPerson.with(contact: contact)「プロパティ」に侵入しすぎている場所を使用している場合-しかし、その連絡先がすでに解析され、関連付けとして識別されている場合、arelが必要なものを簡単に解決できるのは当然のようです...
ジャスティンマクスウェル

14

友達がいない人

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

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

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

Arelでこれを行うには、スコープを Friend

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)

2
私もできると思います:Person.includes(:friends).where(friends:{person:nil})
ReggieB

1
注:マージ戦略では、次のような警告が表示されることがありますDEPRECATION WARNING: It looks like you are eager loading table(s) Currently, Active Record recognizes the table in the string, and knows to JOIN the comments table to the query, rather than loading comments in a separate query. However, doing this without writing a full-blown SQL parser is inherently flawed. Since we don't want to write an SQL parser, we are removing this functionality. From now on, you must explicitly tell Active Record when you are referencing a table from a string
genkilabs

12

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()...私はPerson、NOのレコードを探すために電話をかける必要がないことに気付きました。そのContactsため、それらNOT INは連絡先のリストである必要があるだけですperson_ids。だから私はこのスコープを試しました:

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

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

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

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


5

残念ながら、おそらく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)


1

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

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

1

また、たとえば、1人の友人によってフィルタリングするには:

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

3
これにより、サブクエリではなく2つのクエリが生成されます。
grepsedawk 2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.