Хочу найти записи без связанных записей в рельсах

StackOverflow https://stackoverflow.com/questions/5319400

  •  24-10-2019
  •  | 
  •  

Вопрос

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

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

Я не хочу вытаскивать всех людей. Причудливые записи и проходить через них в Ruby - я хочу иметь запрос/сферу, который я могу использовать с драгоценным камнем Meta_Search

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

И чем дальше от фактического SQL, тем лучше ...

Это было полезно?

Решение

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

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

Другие советы

Лучше:

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

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

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

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

У Smathy есть хороший ответ Rails 3.

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

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

Проверьте API документы. Анкет Это было введено в запросе на притяжение #12071.

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

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)

Оба ответа от 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 мс / запрос

Подход Darkow (:without_friends_v2) 891 мс / запрос (~ 10% медленнее)

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

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

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

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

Спасибо за вашу помощь

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

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

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

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

Friend.where.not(id: other_friend.friends.pluck(:id))
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top