2013-03-08 48 views
10

我试图将每个播放列表的第一首歌曲加入播放列表阵列,并且在寻找有效解决方案时遇到了相当困难的时间。从一个有很多通过协会获得第一个协会

我有以下型号:

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 

这是接近,但我需要的第一首歌曲(根据ID)的名字。

+0

想要加入每首歌曲的播放列表是什么?包含这首歌曲的所有播放列表? – Leito 2013-03-08 19:46:43

+0

您是否尝试搜索具有给定名称和给定歌曲的所有播放列表,或者是否只想输出第一首歌曲旁边所有playlist_name的数组? – 2013-03-08 19:50:44

+0

@rocketscientist后来。他们的第一首歌曲旁边的playlist_names – mrabin 2013-03-08 20:08:39

回答

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 2013-03-15 00:28:23

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 2013-03-08 21:22:08

+0

以上是我将如何做,但你也可以使用[.zip()](http://www.ruby-doc.org/core-1.9.3/Array.html#method-i-zip)if两个阵列的长度都是相同的,顺序也是正确的。数组的'.join()'方法返回一个通过将数组中的每个元素转换为一个字符串而创建的字符串,并由您传递的任何内容分隔开来。方法[.joins](http://guides.rubyonrails.org/active_record_querying.html#joining-tables)是您使用它的方式,是一种用于在SQL中指定JOIN子句的finder方法。 – 2013-03-08 21:40:12

+0

我的意思是.joins不是.join。对困惑感到抱歉。 – mrabin 2013-03-08 21:46:34

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

我认为这个问题可以通过更严格的“第一”定义来改善。我建议在PlaylistSong模型添加position场。在这一点,那么你可以简单地做:

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

我宁愿不将位置存储在PlaylistSong表格中。我将“第一首歌曲”定义为最低编号的歌曲 – mrabin 2013-03-12 17:27:23

+0

您的用户当然希望能够对其播放列表重新排序。然后你需要跟踪用户定义的位置。 :) – 2013-03-12 17:30:25

+0

你是对的,未来可能是真的,但目前播放列表和歌曲是按原样存储的,订单不会改变。我需要一个解决方案,可以与我们现在拥有的产品一起工作。 – mrabin 2013-03-12 17:39:09

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 select语句
  • 不加载所有playlist_songs - 在数据库端聚合
  • 不加载所有歌曲 - 通过ID过滤d b侧

经过MySQL测试。

这不会显示空的播放列表。 ,而且有可能出现问题的一些数据块时播放计数> 1000

0

刚刚从对方获取歌曲:

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

然而,由于@戴夫S.建议,“第一”的歌曲在播放列表中是随机的,除非你明确地指定了一个订单(position列,或其他任何东西),因为SQL不保证记录返回的顺序,除非你明确地要求它。

编辑

对不起,我误解你的问题。我认为确实需要一个职位专栏。

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

如果你不希望任何位置列在所有的,你总是可以尝试按播放列表标识的歌曲,但你必须select("songs.*, playlist_songs.*"),和“第一”的歌曲依然是随机的。另一种选择是使用RANKwindow function,但它不被所有RDBMS支持(对于我所知道的,postgres和sql server都是这样)。

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

这不起作用,因为Song型号 – 2013-03-12 08:53:31

+0

中没有playlist_id字段。我的眼睛完全跳过了连接表。你仍然可以使用'has_one',因为它有一个'through'选项,就像我更新后的答案一样。 – jvnill 2013-03-12 09:00:57

+0

我喜欢这个方向,但我得到以下错误:“:通过关联'播放列表#playlist_cover'是:通过关联'播放列表#playlist_songs'是一个集合”,这是有道理的。 或者你建议创建一个名为playlist_song_cover的新表? – mrabin 2013-03-12 17:19:50

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 

请注意,您可能还需要添加一些范围播放列表歌曲关联模型或播放列表模型。

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方法。

我意识到您正在寻找一种有效的方式来做到这一点,并且您在评论中表示您不想使用块,并且我不确定您的效率是否意味着一个全功能的查询。我只是习惯于将所有这些轨道查询方法结合起来,也许看看我在这里有什么,可以根据需要修改事物并使其更有效或更精简。

希望这会有所帮助。