通过关联从has_many获取第一个关联

时间:2013-03-08 18:45:00

标签: ruby-on-rails ruby join associations

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

我有以下型号:

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

10 个答案:

答案 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

此解决方案有一些好处:

  • 限于4个选择陈述
  • 不加载所有playlist_songs - 聚合在db side
  • 不加载所有歌曲 - 按数据库侧的ID进行过滤

使用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查询方法,也许看看我在这里有什么,你可以根据需要修改东西,使它们更有效或更精简。

希望这有帮助。