我正在尝试将每个播放列表中的第一首歌加入到一系列播放列表中,并且我很难找到有效的解决方案。
我有以下型号:
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)的名字。
答案 0 :(得分:10)
假设您使用的是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
答案 1 :(得分: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]]
答案 2 :(得分:2)
我认为通过对“第一”进行更严格的定义可以改善这个问题。我建议在position
模型上添加PlaylistSong
字段。然后,您可以简单地执行以下操作:
Playlist.joins(:playlist_song).joins(:song).where(:position => 1)
答案 3 :(得分: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
或其他内容的最后添加playlists.*, songs.*
。
答案 4 :(得分: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
此解决方案有一些好处:
使用MySQL进行测试。
这不会显示空的播放列表。 当播放列表计数时,某些数据库可能会出现问题。 1000
答案 5 :(得分: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
window function,但并不是所有RDBMS都支持它(据我所知,postgres和sql server都可以。)
答案 6 :(得分: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
答案 7 :(得分: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"]]
答案 8 :(得分:0)
您可以在模型中添加activerecord scope,以便在应用程序的上下文中优化sql查询的工作方式。此外,范围是可组合的,因此更容易获得您正在寻找的内容。
例如,在Song模型中,您可能需要first_song范围
class Song < ActiveRecord::Base
scope :first_song, order("id asc").limit(1)
end
然后你可以做这样的事情
playlists.songs.first_song
注意,您可能还需要为PlaylistSongs关联模型或播放列表模型添加一些范围。
答案 9 :(得分: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查询方法,也许看看我在这里有什么,你可以根据需要修改东西,使它们更有效或更精简。
希望这有帮助。