11

我正在尝试将每个播放列表的第一首歌曲加入到一系列播放列表中,并且很难找到有效的解决方案。

我有以下型号:

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

我很难通过加入找到一种有效的方法来做到这一点。

更新* ***

Shane Andrade 带领我走向了正确的方向,但我仍然无法得到我想要的。

这是我所能得到的:

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)的名称。

4

10 回答 10

12

假设你在 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
于 2013-03-14T22:01:59.777 回答
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]]

于 2013-03-08T20:21:35.397 回答
2

我认为通过对“第一”进行更严格的定义可以改善这个问题。我建议在模型上添加一个position字段。PlaylistSong此时您可以简单地执行以下操作:

Playlist.joins(:playlist_song).joins(:song).where(:position => 1)
于 2013-03-12T00:26:30.290 回答
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表,因为我们需要歌曲名称。我不确定 Rails 是否足够聪明,可以选择song最后一次连接的字段。如果不是,您可能需要select在选择的末尾包含 aplaylists.*, songs.*或其他内容。

于 2013-03-12T00:20:06.213 回答
0

尝试:

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 过滤

用 MySQL 测试。

这不会显示空的播放列表。当播放列表计数 > 1000 时,某些数据库可能会出现问题

于 2013-03-12T08:02:09.720 回答
0

只需从另一边获取歌曲:

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} )

如果您根本不想要任何位置列,您可以随时尝试按播放列表 id 对歌曲进行分组,但您必须这样做select("songs.*, playlist_songs.*"),并且“第一首”歌曲仍然是随机的。另一种选择是使用RANK 窗口函数,但并非所有 RDBMS 都支持它(据我所知,postgres 和 sql server 都支持)。

于 2013-03-12T08:22:54.070 回答
0

您可以创建一个 has_one 关联,它实际上将调用与播放列表关联的第一首歌曲

class PlayList < ActiveRecord::Base
  has_one :playlist_cover, class_name: 'Song', foreign_key: :playlist_id
end

然后就使用这个关联。

Playlist.joins(:playlist_cover)

更新:没有看到连接表。

如果你有一个连接表,你可以使用一个:through选项has_one

class PlayList < ActiveRecord::Base
  has_one :playlist_song_cover
  has_one :playlist_cover, through: :playlist_song_cover, source: :song
end
于 2013-03-12T08:24:07.917 回答
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"]] 
于 2013-03-13T11:36:28.563 回答
0

您可以将activerecord 范围添加到模型中,以优化 sql 查询在应用程序上下文中为您工作的方式。此外,范围是可组合的,因此更容易获得您正在寻找的东西。

例如,在您的 Song 模型中,您可能需要 first_song 范围

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

然后你可以做这样的事情

  playlists.songs.first_song

请注意,您可能还需要向 PlaylistSongs 关联模型或 Playlist 模型添加一些范围。

于 2013-03-13T20:27:45.313 回答
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],以此类推。我相信您可以使用内置的 ruby​​ 方法很容易地将它们组合成散列或数组。

我意识到您正在寻找一种有效的方法来执行此操作,并且您在评论中说您不想使用块,我不确定您所说的高效是否意味着一体化查询。我只是习惯于结合所有这些 Rails 查询方法,也许看看我在这里的内容,您可以根据自己的需要进行修改,使它们更高效或更简洁。

希望这可以帮助。

于 2013-03-15T00:02:16.593 回答