Получить первую ассоциацию из a имеет много связей
Я пытаюсь присоединиться к первой песне каждого плейлиста к множеству плейлистов, и мне очень сложно найти эффективное решение.
У меня есть следующие модели:
class Playlist < ActiveRecord::Base
belongs_to :user
has_many :playlist_songs
has_many :songs, :through => :playlist_songs
end
class PlaylistSong < ActiveRecord::Base
belongs_to :playlist
belongs_to :song
end
class Song < ActiveRecord::Base
has_many :playlist_songs
has_many :playlists, :through => :playlist_songs
end
Я хотел бы получить следующее:
playlist_name | song_name
----------------------------
chill | baby
fun | bffs
Мне очень сложно найти эффективный способ сделать это через соединение.
ОБНОВЛЕНИЕ ****
Шейн Андраде привел меня в правильном направлении, но я все еще не могу получить именно то, что хочу.
Это насколько мне удалось:
playlists = Playlist.where('id in (1,2,3)')
playlists.joins(:playlist_songs)
.group('playlists.id')
.select('MIN(songs.id) as song_id, playlists.name as playlist_name')
Это дает мне:
playlist_name | song_id
---------------------------
chill | 1
Это близко, но мне нужна первая песня (согласно id).
Ответы
Ответ 1
Предполагая, что вы находитесь в Postgresql
Playlist.
select("DISTINCT ON(playlists.id) playlists.id,
songs.id song_id,
playlists.name,
songs.name first_song_name").
joins(:songs).
order("id, song_id").
map do |pl|
[pl.id, pl.name, pl.first_song_name]
end
Ответ 2
То, что вы делаете выше с помощью объединений, - это то, что вы сделали бы, если бы захотели найти каждый плейлист с заданным именем и данной песней. Чтобы собрать имя плейлиста и первое имя песни из каждого плейлиста, вы можете сделать это:
Playlist.includes(:songs).all.collect{|play_list| [playlist.name, playlist.songs.first.name]}
Это возвращает массив в этой форме [[playlist_name, first song_name],[another_playlist_name, first_song_name]]
Ответ 3
Я думаю, что эта проблема будет улучшена за счет более строгого определения "first". Я бы предложил добавить поле position
в PlaylistSong
. В этот момент вы можете просто сделать:
Playlist.joins(:playlist_song).joins(:song).where(:position => 1)
Ответ 4
Я думаю, что лучший способ сделать это - использовать внутренний запрос, чтобы получить первый элемент, а затем присоединиться к нему.
Неподтвержденный, но это основная идея:
# gnerate the sql query that selects the first item per playlist
inner_query = Song.group('playlist_id').select('MIN(id) as id, playlist_id').to_sql
@playlists = Playlist
.joins("INNER JOIN (#{inner_query}) as first_songs ON first_songs.playlist_id = playlist.id")
.joins("INNER JOIN songs on songs.id = first_songs.id")
Затем вернитесь в таблицу songs
, так как нам нужно имя песни. Я не уверен, что рельсы достаточно умны, чтобы выбрать поля song
в последнем соединении. Если нет, вам может понадобиться включить select
в конец, который выбирает playlists.*, songs.*
или что-то еще.
Ответ 5
Try:
PlaylistSong.includes(:song, :playlist).
find(PlaylistSong.group("playlist_id").pluck(:id)).each do |ps|
puts "Playlist: #{ps.playlist.name}, Song: #{ps.song.name}"
end
(0.3ms) SELECT id FROM `playlist_songs` GROUP BY playlist_id
PlaylistSong Load (0.2ms) SELECT `playlist_songs`.* FROM `playlist_songs` WHERE `playlist_songs`.`id` IN (1, 4, 7)
Song Load (0.2ms) SELECT `songs`.* FROM `songs` WHERE `songs`.`id` IN (1, 4, 7)
Playlist Load (0.2ms) SELECT `playlists`.* FROM `playlists` WHERE `playlists`.`id` IN (1, 2, 3)
Playlist: Dubstep, Song: Dubstep song 1
Playlist: Top Rated, Song: Top Rated song 1
Playlist: Last Played, Song: Last Played song 1
Это решение имеет некоторые преимущества:
- Ограничено 4 операциями выбора
- Не загружает все playlist_songs - агрегирование на стороне db
- Не загружает все песни - фильтрация по id на стороне db
Протестировано с помощью MySQL.
Это не покажет пустые плейлисты.
И могут быть проблемы с некоторыми БД, когда списки воспроизведения > 1000
Ответ 6
просто выберите песню с другой стороны:
Song
.joins( :playlist )
.where( playlists: {id: [1,2,3]} )
.first
однако, как предположил @Dave S., "первая" песня в плейлисте случайна, если вы явно не указали порядок (столбец position
или что-то еще), потому что SQL не гарантирует порядок, в котором возвращаются записи, если вы явно не спросите об этом.
ИЗМЕНИТЬ
Извините, я неправильно понял ваш вопрос. Я думаю, что действительно необходим столбец позиции.
Song
.joins( :playlist )
.where( playlists: {id: [1,2,3]}, songs: {position: 1} )
Если вам не нужен какой-либо столбец позиции, вы всегда можете попытаться сгруппировать песни по идентификатору списка воспроизведения, но вам нужно будет select("songs.*, playlist_songs.*")
, а "первая" песня все равно случайна. Другим вариантом является использование функции RANK
окна, но она не поддерживается всеми RDBMS (для всех, что я знаю, postgres и sql server делают).
Ответ 7
вы можете создать ассоциацию has_one, которая, по сути, вызовет первую песню, связанную с плейлистом
class PlayList < ActiveRecord::Base
has_one :playlist_cover, class_name: 'Song', foreign_key: :playlist_id
end
Тогда просто используйте эту ассоциацию.
Playlist.joins(:playlist_cover)
UPDATE: не видел таблицу соединений.
вы можете использовать опцию :through
для has_one
, если у вас есть таблица соединений
class PlayList < ActiveRecord::Base
has_one :playlist_song_cover
has_one :playlist_cover, through: :playlist_song_cover, source: :song
end
Ответ 8
Playlyst.joins(:playlist_songs).group('playlists.name').minimum('songs.name').to_a
надеюсь, что он работает:)
получил следующее:
Product.includes(:vendors).group('products.id').collect{|product| [product.title, product.vendors.first.name]}
Product Load (0.5ms) SELECT "products".* FROM "products" GROUP BY products.id
Brand Load (0.5ms) SELECT "brands".* FROM "brands" WHERE "brands"."product_id" IN (1, 2, 3)
Vendor Load (0.4ms) SELECT "vendors".* FROM "vendors" WHERE "vendors"."id" IN (2, 3, 1, 4)
=> [["Computer", "Dell"], ["Smartphone", "Apple"], ["Screen", "Apple"]]
2.0.0p0 :120 > Product.joins(:vendors).group('products.title').minimum('vendors.name').to_a
(0.6ms) SELECT MIN(vendors.name) AS minimum_vendors_name, products.title AS products_title FROM "products" INNER JOIN "brands" ON "brands"."product_id" = "products"."id" INNER JOIN "vendors" ON "vendors"."id" = "brands"."vendor_id" GROUP BY products.title
=> [["Computer", "Dell"], ["Screen", "Apple"], ["Smartphone", "Apple"]]
Ответ 9
Вы можете добавить область activerecord к вашим моделям, чтобы оптимизировать, как SQL-запросы работают для вас в контексте приложения. Кроме того, области могут быть скомпонованы, что облегчает получение того, что вы ищете.
Например, в вашей модели песни вам может понадобиться область first_song
class Song < ActiveRecord::Base
scope :first_song, order("id asc").limit(1)
end
И тогда вы можете сделать что-то вроде этого
playlists.songs.first_song
Примечание. Вам также может потребоваться добавить некоторые области действия в вашу модель ассоциации PlaylistSongs или в вашу модель Playlist.
Ответ 10
Вы не сказали, были ли у вас временные метки в вашей базе данных. Если вы это сделаете, и ваши записи в таблице соединений PlaylistSongs создаются при добавлении песни в список воспроизведения, я думаю, что это может сработать:
first_song_ids = Playlist.joins(:playlist_songs).order('playlist_songs.created_at ASC').pluck(:song_id).uniq
playlist_ids = Playlist.joins(:playlist_songs).order('playlist_songs.created_at ASC').pluck(:playlist_id).uniq
playlist_names = Playlist.where(id: playlist_ids).pluck(:playlist_name)
song_names = Song.where(id: first_song_ids).pluck(:song_name)
Я считаю, что playlist_names и song_names теперь отображаются их индексом таким образом. Как в: playlist_names [0] имя первой песни - song_names [0], а playlist_names [1] имя первой песни - song_names [1] и т.д. Я уверен, что вы можете легко комбинировать их в хэше или массиве со встроенными рубиновыми методами.
Я понимаю, что вы искали эффективный способ сделать это, и вы сказали в комментариях, что не хотите использовать блок, и я не уверен, что по эффективности вы имели в виду запрос "все-в-одном". Я просто привык к объединению всех этих методов запросов рельсов и, возможно, глядя на то, что у меня есть здесь, вы можете модифицировать вещи в соответствии с вашими потребностями и сделать их более эффективными или сжатыми.
Надеюсь, что это поможет.