2013-03-08 49 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方法。

我意識到您正在尋找一種有效的方式來做到這一點,並且您在評論中表示您不想使用塊,並且我不確定您的效率是否意味着一個全功能的查詢。我只是習慣於將所有這些軌道查詢方法結合起來,也許看看我在這裏有什麼,可以根據需要修改事物並使其更有效或更精簡。

希望這會有所幫助。