Хотите найти записи без связанных записей в Rails 3
Рассмотрим простую ассоциацию...
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:through
Я не хочу извлекать все записи person.friends и перебирать их в Ruby - мне нужен запрос / область, которую я могу использовать с гемом meta_search
Я не против стоимости выполнения запросов
И чем дальше от реального SQL, тем лучше...
8 ответов
Это все еще довольно близко к 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 (дайте ему +1 +1 за ответ ниже), вы можете использовать left_outer_joins
чтобы избежать загрузки ассоциации:
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Я включил это здесь, чтобы люди нашли это, но он заслуживает +1 для этого. Отличное дополнение!
У smathy хороший ответ на Rails 3.
Для Rails 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мс / запрос
подход Маркова (: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)
Вот вариант использования подзапроса:
# Scenario #1 - person <-> friend
people = Person.where.not(id: Friend.select(:person_id))
# Scenario #2 - person <-> contact <-> friend
people = Person.where.not(id: Contact.select(:person_id))
Приведенные выше выражения должны генерировать следующий SQL:
-- Scenario #1 - person <-> friend
SELECT people.*
FROM people
WHERE people.id NOT IN (
SELECT friends.person_id
FROM friends
)
-- Scenario #2 - person <-> contact <-> friend
SELECT people.*
FROM people
WHERE people.id NOT IN (
SELECT contacts.person_id
FROM contacts
)
Кроме того, чтобы отфильтровать по одному другу, например:
Friend.where.not(id: other_friend.friends.pluck(:id))
Коррелированный подзапрос NOT EXISTS должен быть быстрым, особенно по мере того, как увеличивается число строк и соотношение дочерних и родительских записей.
scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")