Поиск объектов только там, где все их коллекции has_many имеют значение для определенного столбца

У меня есть объект Album, в котором много Tracks. Треки имеют столбец youtube_uid. Я хотел бы запросить альбомы, в которых присутствуют все их треки youtube_uids. Я знаю технику поиска альбомов с треками, где есть хотя бы один трек с youtube_uid:

Album.left_outer_joins(:tracks).where.not(tracks: { youtube_uid: nil })

Каким должен быть идеальный запрос для поиска альбома, в котором каждый трек имеет youtube_uid?


person Carl Edwards    schedule 26.08.2020    source источник
comment
Хм, я думаю, вам нужно сделать что-то вроде WHERE NOT EXISTS(SELECT 1 FROM tracks WHERE tracks.youtube_uid IS NULL AND tracks.album_id = album.id).   -  person max    schedule 26.08.2020
comment
@max Похоже, мы приближаемся, но сталкиваемся с синтаксической ошибкой. PG::SyntaxError: ERROR: syntax error at or near "WHERE" (ActiveRecord::StatementInvalid) LINE 1: SELECT "albums".* FROM "albums" WHERE (WHERE NOT EXISTS(SELE...   -  person Carl Edwards    schedule 26.08.2020
comment
@max Кажется, это помогло. Не стесняйтесь публиковать это решение, и я могу отметить это как правильный ответ.   -  person Carl Edwards    schedule 26.08.2020


Ответы (2)


Насколько я понимаю ваш вопрос, вы хотите найти все альбомы, в которых нет трека с отсутствующим (пустым) идентификатором youtube-uid. Итак, вам нужен запрос NOT EXISTS.

В sql я бы написал что-то вроде

 select * from albums a
 where not exists (select * from tracks where album_id = a.id and youtube_uid is null) 

Итак, как нам лучше всего перевести это в activerecord? Я вижу две возможности:

 sql = <<-SQL
   select * from albums a
   where not exists (select * from tracks where album_id = a.id and youtube_uid is null) 
 SQL

 Album.find_by_sql(sql) 

в то время как это работает, и для меня, как дома в SQL, это нормально, это не очень похоже на рельсы, так что мы можем это улучшить?

Есть более короткая форма:

Album.where("not exists (select * from tracks where album_id = albums.id and youtube_uid is null")

но это все еще кажется слишком многословным. К счастью, существует более рельсовый способ. В рельсах 4 вы можете написать:

  Album.where(Track.where("album_id = albums.id").where(youtube_uid: nil).exists.not)

В рельсах 5/6 это уже невозможно, и вы должны написать:

  Album.where(Track.where("album_id = albums.id").where(youtube_uid: nil).arel.exists.not)

вы можете легко убедиться, что это генерирует хороший sql, добавив to_sql в конце.

person nathanvda    schedule 26.08.2020
comment
Запрос надежный, но вы сбиваетесь с пути, когда дело доходит до его адаптации к Rails. Вам не нужно использовать find_by_sql, вы можете просто использовать Album.where('NOT EXISTS ...'). .exists.not будет работать, если вы вызовете его в arel_table, но не будет работать в отношении ActiveRecord. См. stackoverflow.com/questions/7152424/rails -3-относительно-несуществования - person max; 26.08.2020
comment
Спасибо. Я столкнулся с этой ошибкой: `неопределенный метод exists' for #<Track::ActiveRecord_Relation:0x00007fd461f90de8> (NoMethodError) Did you mean? exists?. Попробовав предложенный метод, я получил: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "albums" (ActiveRecord::StatementInvalid) LINE 1: SELECT "tracks".* FROM "tracks" WHERE (album_id = albums.id).... Как вы думаете, мне нужно предложение присоединения? - person Carl Edwards; 26.08.2020
comment
Спасибо @max хорошие предложения. сбиться с пути, вероятно, немного сложно, find_by_sql действительно работает. Но правильно, я проверил это на рельсах 4, где это работает. В рельсах 5/6 вы должны написать where(...).arel.exists. Я обновил ответ соответственно. - person nathanvda; 26.08.2020
comment
Я согласен, что заблуждение было немного суровым. Однако результаты find_by_sql менее полезны, так как они возвращают необработанные результаты запроса вместо объекта ActiveRecord::Relation. - person max; 26.08.2020
comment
Чтобы быть явным: find_by_sql действительно возвращает Albums (поэтому не совсем сырой), но да, он больше не может быть привязан к цепочке, что делает его действительно намного менее полезным. Так что определенно мы бы/должны стараться избегать использования find_by_sql; но в сложных случаях я сначала пытаюсь решить это с помощью SQL, а затем пытаюсь написать в ActiveRecord (если возможно) (так что я также показывал свой мыслительный процесс, который, вероятно, слишком много информации). - person nathanvda; 26.08.2020
comment
Стоит отметить, что при использовании обратного EXISTS, а не внутреннего соединения, этот запрос также вернет альбомы вообще без треков; он также пропускает любые области видимости, явно определенные в отношении Album::tracks. - person inopinatus; 27.08.2020

Мы добьемся этого с помощью Group by и имея также:

Album.left_outer_joins(:tracks).group(:id).having('COUNT(tracks.id) = COUNT(tracks.youtube_uid)')
person user11350468    schedule 26.08.2020