Хотите найти записи без связанных записей в Rails - PullRequest
159 голосов
/ 16 марта 2011

Рассмотрим простую ассоциацию ...

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

Какой самый чистый способ собрать всех людей, у которых НЕТ друзей в ARel и / или meta_where?

А что насчетhas_many: через версию

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: через

Я нехочу получить все записи person.friends и просмотреть их в Ruby - мне нужен запрос / область, которую я могу использовать с гемом meta_search

Я не возражаю против затрат производительности запросов

И чем дальше от реального SQL, тем лучше ...

Ответы [ 8 ]

397 голосов
/ 06 апреля 2011

Лучше:

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

Для хмт это в основном то же самое, вы полагаетесь на то, что у человека без друзей также не будет контактов:

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

Благодаря @Anson за отличное решение для Rails 5 (дайте ему + 1с за ответ ниже), вы можете использовать left_outer_joins, чтобы избежать загрузки ассоциации:

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

Я включил его сюда, чтобы люди его нашли, но он заслуживает +1 за это.Отличное дополнение!

133 голосов
/ 09 ноября 2016

smathy имеет хороший ответ по Rails 3.

Для Rails 5 вы можете использовать left_outer_joins, чтобы избежать загрузки ассоциации. API документы .Он был введен в запрос на получение # 12071 .

97 голосов
/ 16 марта 2011

Это все еще довольно близко к SQL, но в первом случае это должно заставить всех без друзей:

Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
13 голосов
/ 29 сентября 2013

Лица, у которых нет друзей

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

или у которых есть хотя бы один друг

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

А потом, Лица, у которых есть хотя бы один друг:

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

Без друзей:

Person.includes(:friends).merge(Friend.to_nobody)
11 голосов
/ 16 марта 2011

Оба ответа от dmarkow и Unixmonkey дают мне то, что мне нужно - спасибо!

Я опробовал оба в своем реальном приложении и получил время для них - вот две области:

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 записями «Персона» - в среднем за 5 прогонов

Подход Unixmonkey (:without_friends_v1) 813мс / запрос

Подход dmarkow (:without_friends_v2) 891мс / запрос (на 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 голосов
/ 16 марта 2011

К сожалению, вы, вероятно, ищете решение, включающее SQL, но вы могли бы установить его в области, а затем просто использовать эту область:1004 *, и вы также можете связать это с другими методами Arel: Person.without_friends.order("name").limit(10)

1 голос
/ 02 июня 2017

Также, чтобы отфильтровать по одному другу, например:

Friend.where.not(id: other_friend.friends.pluck(:id))
1 голос
/ 16 сентября 2013

Коррелированный подзапрос NOT EXISTS должен быть быстрым, особенно с увеличением числа строк и отношения дочерних и родительских записей.

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