Хотите найти записи без связанных записей в Rails

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

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, тем лучше...

Ответ 1

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

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

Ответ 2

лучше:

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

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

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

Update

В комментариях задан вопрос о has_one, поэтому просто обновляем. Фокус здесь в том, что includes() ожидает имя ассоциации, но where ожидает имя таблицы. При a has_one ассоциация обычно будет выражаться в единственном числе, так что изменения, но часть where() остается такой, какая есть. Поэтому, если a Person only 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 - Рельсы 5

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

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

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

Ответ 3

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

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

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

Просмотрите api docs. Он был введен в запрос тяги # 12071.

Ответ 4

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

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)

Ответ 5

Оба ответа от 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 "Записью Person" - в среднем 5 прогонов

Подход Unixmonkey (:without_friends_v1) 813ms/query

подход dmarkow (:without_friends_v2) 891ms/query (~ 10% медленнее)

Но потом мне пришло в голову, что мне не нужен вызов DISTINCT()... Я ищу записи Person с NO Contacts - поэтому они просто должны быть NOT IN список контактов person_ids. Поэтому я пробовал эту область:

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

Получается тот же результат, но со средним значением 425 мс/звонок - почти в половину времени...

Теперь вам может понадобиться DISTINCT в других похожих запросах, но для моего случая это, кажется, работает нормально.

Спасибо за помощь

Ответ 6

К сожалению, вы, вероятно, смотрите на решение с 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)

Ответ 7

НЕ СУЩЕСТВУЕТ, что коррелированный подзапрос должен быть быстрым, особенно когда количество строк и отношение дочерних и родительских записей увеличивается.

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

Ответ 8

Кроме того, для фильтрации одним другом, например:

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