Ruby on rails 从具有多个直通关联的服务器获取第一个关联
我正在尝试将每个播放列表的第一首歌曲加入到一系列播放列表中,并且很难找到一个有效的解决方案 我有以下型号:Ruby on rails 从具有多个直通关联的服务器获取第一个关联,ruby-on-rails,ruby,join,associations,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
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
我很难找到一个有效的方法通过加入来实现这一点
更新***
谢恩·安德拉德带领我朝着正确的方向前进,但我仍然无法得到我想要的
这是我所能做到的:
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)的名称。如果要查找具有给定名称和给定歌曲的每个播放列表,您在上面使用连接所做的就是。要从每个播放列表中收集播放列表名称和第一首歌曲名称,可以执行以下操作:
Playlist.includes(:songs).all.collect{|play_list| [playlist.name, playlist.songs.first.name]}
这将以这种形式返回一个数组
[[playlist\u name,first song\u name],[other\u playlist\u name,first\u song\u name]]
我认为最好的方法是使用内部查询获取第一个项目,然后加入其中
未经测试,但这是基本理念:
# 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")
然后重新加入
歌曲
表,因为我们需要歌曲名称。我不确定rails是否足够聪明,可以选择上次加入时的song
字段。如果没有,您可能需要在选择播放列表、歌曲或其他内容的末尾添加一个select
。我认为如果对“第一”有一个更严格的定义,这个问题就会得到改善。我建议在播放歌曲
模型上添加一个位置
字段。此时,您可以简单地执行以下操作:
Playlist.joins(:playlist_song).joins(:song).where(:position => 1)
尝试:
此解决方案有一些好处:
- 限制为4条select语句
- 不加载所有播放列表歌曲-在数据库端聚合
- 不加载所有歌曲-在db端按id过滤
当播放列表计数>1000时,某些DBs可能会出现问题,只需从另一端获取歌曲即可:
Song
.joins( :playlist )
.where( playlists: {id: [1,2,3]} )
.first
然而,正如@Dave S.所建议的,播放列表中的“第一首”歌曲是随机的,除非您明确指定顺序(位置
列或任何其他内容),因为SQL不保证返回记录的顺序,除非您明确要求
编辑
对不起,我误解了你的问题。我认为确实有必要设立一个职位专栏
Song
.joins( :playlist )
.where( playlists: {id: [1,2,3]}, songs: {position: 1} )
如果您根本不需要任何位置列,您可以尝试按播放列表id对歌曲进行分组,但您必须
选择(“歌曲。*,播放列表_歌曲。*”)
,并且“第一首”歌曲仍然是随机的。另一种选择是使用排名
,但并非所有RDBMS都支持它(据我所知,postgres和sql server都支持)。您可以创建一个has\u one关联,该关联实际上将调用与播放列表关联的第一首歌曲
class PlayList < ActiveRecord::Base
has_one :playlist_cover, class_name: 'Song', foreign_key: :playlist_id
end
更新:未看到联接表
如果有联接表,则可以使用:至选项来选择是否有一个
class PlayList < ActiveRecord::Base
has_one :playlist_song_cover
has_one :playlist_cover, through: :playlist_song_cover, source: :song
end
class PlayList
希望它能起作用:)
得到这个:
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"]]
您可以添加到模型中,以优化sql查询在应用程序上下文中的工作方式。此外,作用域是可组合的,因此更容易获得所需的内容
例如,在歌曲模型中,您可能需要第一首歌曲范围
class Song < ActiveRecord::Base
scope :first_song, order("id asc").limit(1)
end
注意,您可能还需要将一些作用域添加到播放歌曲关联模型或播放列表模型中。假设您使用的是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
你没有说你的数据库中是否有时间戳。如果您这样做了,并且在将歌曲添加到播放列表时创建了加入表播放歌曲上的记录,我认为这可能会起作用:
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[0]首歌名为song_names[0],playlist_names[1]首歌名为song_names[1],依此类推。我相信您可以使用内置的ruby方法很容易地将它们组合到哈希或数组中
我知道你在寻找一种高效的方法来实现这一点,你在评论中说你不想使用块,我不确定你所说的高效是否意味着一个一体化的查询。我刚刚习惯于组合所有这些rails查询方法,也许看看我这里的内容,您可以根据需要修改内容,使它们更高效或更精简
希望这能有所帮助。您希望加入到每首歌曲中的播放列表是什么?包含此歌曲的所有播放列表?您是要搜索具有给定名称和给定歌曲的所有播放列表,还是只想在第一首歌曲旁边输出所有播放列表名称的数组?@rocketscientist the later。播放列表\u首字母旁边的姓名song@Leito我只想看看每个播放列表中的第一首歌你说的“高效”是什么意思?你的查询有效吗?DB如何知道播放列表中的第一首歌曲?这是可行的,但我希望有某种方法可以通过加入来做到这一点。上面是我将如何做到的,但如果两个数组长度相同且顺序正确,您也可以使用。数组的.join()
方法返回一个字符串,该字符串是通过将数组中的每个元素转换为字符串创建的,由传递的内容分隔。您使用的方法是一个查找器方法,用于在SQL中指定联接子句。我的意思是.joins而不是.JOIN。抱歉造成混淆。这将导致N+1查询。。。如果您显示100个播放列表,这将导致101个sql查询。@ShaneAndrade,不,它不会:Playlist.includes(:songs).all.collect{Playlist}[Playlist.name,Playlist.songs.first.name]}播放列表加载(0.3ms)选择播放列表
*FROM播放列表
播放歌曲加载(0.4ms)SEL
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"]]
class Song < ActiveRecord::Base
scope :first_song, order("id asc").limit(1)
end
playlists.songs.first_song
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
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)