2013-03-08 3 views
10

Я пытаюсь присоединиться к первой песне каждого плейлиста к множеству плейлистов, и мне довольно сложно найти эффективное решение.Получить первую ассоциацию из 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 

У меня довольно трудное время найти эффективный способ сделать это через объединение.

UPDATE * ***

Шейн Андраде привел меня в правильном направлении, но я до сих пор не могу получить именно то, что я хочу.

Это, насколько я был в состоянии получить:

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 

Это близко, но мне нужна первая песня (по идентификатору) имя.

+0

Какой массив плейлистов вы хотите присоединиться к каждой песне? Все плейлисты, которые содержат эту песню? – Leito

+0

Вы пытаетесь найти все плейлисты с заданным именем и определенной песней. Или вы хотите просто вывести массив всех плейлистов по первой песне? –

+0

@rocketscientist позже. playlist_names рядом со своей первой песней – mrabin

ответ

9

Предполагая, что вы находитесь на 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 
+0

Это именно то, что я искал, спасибо. Раньше я никогда не делал щедрости. Принимаю ли я сейчас и отдаю его вам или люди обычно ждут, когда закончится время? – mrabin

2

То, что вы делаете выше с помощью объединений, - это то, что вы сделали бы, если бы захотели найти каждый плейлист с заданным именем и данной песней. Для того, чтобы собрать PLAYLIST_NAME и первый SONG_NAME из каждого списка вы можете сделать это:

Playlist.includes(:songs).all.collect{|play_list| [playlist.name, playlist.songs.first.name]} 

Это возвращает массив в таком виде [[playlist_name, first song_name],[another_playlist_name, first_song_name]]

+0

Это работает, но я ожидал, что будет какой-то способ сделать это через соединение. – mrabin

+0

Выше было бы так, как я это сделал бы, но вы также можете использовать [.zip()] (http://www.ruby-doc.org/core-1.9.3/Array.html#method-i-zip), если оба массива имеют одинаковую длину и в правильном порядке. Метод '.join()' для массива возвращает строку, созданную путем преобразования каждого элемента массива в строку, разделенной тем, что вы передаете. Метод [.joins] (http://guides.rubyonrails.org/active_record_querying.html#joining-tables), как вы его использовали, - это метод finder для указания предложений JOIN в SQL. –

+0

Я имел в виду .joins не .join. Извините за путаницу. – mrabin

0

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

непроверенная, но это основная идея:

# 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.* или что-то в этом роде.

2

Я думаю, что эта проблема будет улучшена путем определения более строгой «первой». Я бы предложил добавить поле position на модели PlaylistSong.В этот момент вы можете просто сделать:

Playlist.joins(:playlist_song).joins(:song).where(:position => 1) 
+0

Я бы предпочел не сохранять позицию в таблице PlaylistSong. Я определяю «первую песню» как песню с самым низким id – mrabin

+0

Конечно, ваши пользователи хотели бы изменить порядок своих плейлистов. Затем вам нужно отслеживать пользовательскую позицию. :) –

+0

Вы правы, в будущем это может быть правдой, но в настоящий момент плейлисты и песни хранятся как есть, и порядок не меняется. Мне нужно решение, которое будет работать с тем, что у нас есть. – mrabin

0

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 

Это решение имеет некоторые преимущества:

  • Limited на 4 операторов выбора
  • не нагружает все playlist_songs - агрегирование на стороне db
  • Не загружает все песни - фильтрация по id на d b сторона

Протестировано с MySQL.

Это не покажет пустые плейлисты. И могут возникнуть проблемы с некоторыми БД, когда плейлистам число> 1000

0

просто принести песню с другой стороны:

Song 
    .joins(:playlist) 
    .where(playlists: {id: [1,2,3]}) 
    .first 

однако, как это было предложено С. @ Dave, «первый» песню в плейлист является случайным, если вы явно не указали порядок (position столбец или что-то еще), потому что SQL не гарантирует порядок, в котором возвращаются записи, если вы явно не зададите его.

EDIT

К сожалению, я неправильно понял ваш вопрос. Я думаю, что действительно необходим столбец позиции.

Song 
    .joins(:playlist) 
    .where(playlists: {id: [1,2,3]}, songs: {position: 1}) 

Если вы не хотите какой-либо столбец позиции на всех, вы всегда можете попробовать сгруппировать песни по плейлистам ид, но вы должны будете select("songs.*, playlist_songs.*"), и «первый» песня остается случайным. Другой вариант - использовать RANKwindow function, но он не поддерживается всеми РСУБД (для всех, что я знаю, postgres и sql server do).

0

вы можете создать 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 
+0

Это не сработает, так как нет поля playlist_id в модели песни –

+0

извините. мои глаза полностью пропустили этот стол. вы все равно можете использовать 'has_one', но поскольку он имеет опцию' through', как в моем обновленном ответе. – jvnill

+0

Мне нравится направление на этом, но я получаю следующую ошибку: «: через ассоциацию« Playlist # playlist_cover »были: через ассоциацию« Playlist # playlist_songs »- это коллекция», что имеет смысл. Или где вы предлагаете создать новую таблицу под названием playlist_song_cover? – mrabin

0
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"]] 
0

Вы можете добавить activerecord scope для ваших моделей, чтобы оптимизировать работу запросов sql для вас в контексте приложения. Кроме того, области могут быть скомпонованы, что облегчает получение того, что вы ищете.

Например, в модели песни, вы можете захотеть first_song прицел

class Song < ActiveRecord::Base 
    scope :first_song, order("id asc").limit(1) 
end 

И тогда вы можете сделать что-то вроде этого

playlists.songs.first_song 

Примечание, вы можете также необходимо добавить некоторые областей к вашей модели ассоциации PlaylistSongs или к вашей модели Playlist.

0

Вы не сказали, если вы имели метки времени в базе данных. Если вы делаете, хотя, и ваши записи на объединение таблиц 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] и т. Д. Я уверен, что вы можете легко комбинировать их в хэше или массиве со встроенными рубиновыми методами.

Я понимаю, что вы искали эффективный способ сделать это, и вы сказали в комментариях, что не хотите использовать блок, и я не уверен, что по эффективности вы имели в виду запрос «все-в-одном». Я просто привык к объединению всех этих методов запросов рельсов и, возможно, глядя на то, что у меня есть здесь, вы можете модифицировать вещи в соответствии с вашими потребностями и сделать их более эффективными или сжатыми.

Надеюсь, это поможет.

Смежные вопросы