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

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

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