2013-03-08 7 views
10

Sto provando ad unire la prima canzone di ogni playlist a una serie di playlist e sto avendo un momento piuttosto difficile trovare una soluzione efficiente.Ottenere la prima associazione da una ha attraverso l'associazione

Ho i seguenti modelli:

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 

vorrei ottenere questo:

playlist_name | song_name 
---------------------------- 
chill   | baby 
fun   | bffs 

Sto avendo un periodo piuttosto difficile trovare un modo efficace per fare questo attraverso un join.

UPDATE * ***

Shane Andrade mi ha portato nella giusta direzione, ma non riesco ancora a ottenere esattamente quello che voglio.

Questo è quanto sono stato in grado di ottenere:

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') 

Questo mi dà:

playlist_name | song_id 
--------------------------- 
chill   | 1 

Questa è vicino, ma ho bisogno la prima canzone (secondo id) il nome

+0

Quale array di playlist vuoi unire a ogni canzone? Tutte le playlist che contengono questa canzone? – Leito

+0

Stai provando a cercare tutte le playlist con un nome e una canzone specifici o vuoi solo produrre una matrice di tutti i nomi di playlist accanto alla loro prima canzone? –

+0

@rocketscientist il più tardi. playlist_names accanto al loro primo brano – mrabin

risposta

9

si Supponendo che sono su 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

Questo è esattamente quello che stavo cercando, grazie. Non ho mai fatto un premio prima. Accetto ora e lo do a te o le persone normalmente aspettano che il tempo si esaurisca? – mrabin

2

Quello che stai facendo in precedenza con i join è quello che faresti se volessi trovare ogni playlist con un nome e una canzone determinati. Al fine di raccogliere il PLAYLIST_NAME e prima SONG_NAME di ogni playlist si può fare questo:

Playlist.includes(:songs).all.collect{|play_list| [playlist.name, playlist.songs.first.name]} 

Ciò restituirà una matrice in questa forma [[playlist_name, first song_name],[another_playlist_name, first_song_name]]

+0

Funziona, ma mi aspettavo che ci fosse un modo per farlo attraverso un join. – mrabin

+0

Sopra sarebbe come lo farei ma puoi anche usare [.zip()] (http://www.ruby-doc.org/core-1.9.3/Array.html#method-i-zip) se entrambi i tuoi array hanno la stessa lunghezza e nell'ordine corretto. Il metodo '.join()' per una matrice restituisce una stringa creata convertendo ogni elemento della matrice in una stringa, separata da qualsiasi cosa tu passi. Il metodo [.joins] (http://guides.rubyonrails.org/active_record_querying.html#joining-tables) nel modo in cui lo stavate usando, è un metodo finder per specificare clausole JOIN in SQL. –

+0

Intendevo .join non .join. Dispiace per la confusione. – mrabin

0

Penso che il modo migliore per farlo è quello di utilizzare un inner query per ottenere il primo elemento e quindi partecipare a esso.

testato ma questa è l'idea di base:

# 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") 

Poi ricongiungersi di nuovo al tavolo songs da quando abbiamo bisogno del nome della canzone. Non sono sicuro che le rotaie siano abbastanza intelligenti da selezionare i campi song nell'ultimo join. In caso contrario, potrebbe essere necessario includere un select alla fine che seleziona playlists.*, songs.* o qualcosa del genere.

2

Penso che questo problema sarebbe migliorato avendo una definizione più rigorosa di "prima". Suggerirei di aggiungere un campo position nel modello PlaylistSong.A quel punto si può poi semplicemente fare:

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

Preferirei non archiviare la posizione nel tavolo PlaylistSong. Sto definendo la "prima canzone" come la canzone con l'id più basso – mrabin

+0

Sicuramente i tuoi utenti vorrebbero la possibilità di riordinare le loro playlist. Quindi è necessario tenere traccia della posizione definita dall'utente. :) –

+0

Hai ragione, in futuro potrebbe essere vero, ma al momento le playlist e le canzoni sono archiviate così come sono e l'ordine non cambia. Ho bisogno di una soluzione che funzioni con ciò che abbiamo ora. – mrabin

0

Prova:

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 

Questa soluzione ha alcuni vantaggi:

  • limitato a 4 select
  • non carica tutte le playlist_songs - aggregazione sul lato db
  • Non carica tutti i brani - filtraggio per id su db b lato

Testato con MySQL.

Questo non mostrerà le playlist vuote. e ci potrebbero essere problemi con alcuni DB quando playlist contano> 1000

0

basta prendere il brano dal lato opposto:

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

tuttavia, come @ Dave S. suggerito, "prima" canzone in una playlist è casuale a meno che non si specifichi esplicitamente un ordine (colonna position o altro) perché SQL non garantisce l'ordine in cui vengono restituiti i record, a meno che non lo si chieda esplicitamente.

EDIT

Siamo spiacenti, ho letto male la tua domanda. Penso che in effetti una colonna di posizione sia necessaria.

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

Se non si desidera una colonna posizione in tutti, si può sempre cercare di raggruppare le canzoni di playlist id, ma si dovrà select("songs.*, playlist_songs.*"), e la "prima" canzone è ancora casuale. Un'altra opzione è usare lo RANKwindow function, ma non è supportato da tutti gli RDBMS (per quanto ne so, postgres e sql server fanno).

0

è possibile creare un'associazione has_one che, in effetti, chiamerà la prima canzone che è associato alla playlist

class PlayList < ActiveRecord::Base 
    has_one :playlist_cover, class_name: 'Song', foreign_key: :playlist_id 
end 

poi basta usare questa associazione.

Playlist.joins(:playlist_cover) 

UPDATE: non ha visto la tabella di join.

è possibile utilizzare un'opzione :through per has_one se si dispone di un join tavolo

class PlayList < ActiveRecord::Base 
    has_one :playlist_song_cover 
    has_one :playlist_cover, through: :playlist_song_cover, source: :song 
end 
+0

Questo non funzionerà in quanto non esiste alcun campo playlist_id nel modello di canzone –

+0

scusa. i miei occhi hanno saltato del tutto quel tavolo. puoi comunque usare 'has_one' dato che ha un'opzione' through' come nella mia risposta aggiornata. – jvnill

+0

Mi piace la direzione, ma ho il seguente errore: ": attraverso l'associazione" Playlist # playlist_cover "c'erano l'associazione:" Playlist # playlist_songs "è una raccolta", il che ha senso. O dove suggerisci di creare una nuova tabella chiamata playlist_song_cover? – mrabin

0
Playlyst.joins(:playlist_songs).group('playlists.name').minimum('songs.name').to_a 

spero che funziona :)

ottenuto questo:

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

Si potrebbe aggiungere activerecord scope ai tuoi modelli per ottimizzare il modo in cui le query sql funzionano per te nel contesto dell'app. Inoltre, gli ambiti sono componibili, rendendo così più facile ottenere ciò che stai cercando.

Ad esempio, nel modello Song, si consiglia un ambito first_song

class Song < ActiveRecord::Base 
    scope :first_song, order("id asc").limit(1) 
end 

E poi si può fare qualcosa di simile

playlists.songs.first_song 

nota, potrebbe anche essere necessario aggiungere alcuni ambiti al modello dell'associazione PlaylistSongs o al modello della playlist.

0

Lei non ha detto se si ha timestamp nel database. Se lo fai, però, e le vostre annotazioni su PlaylistSongs tabella vengono creati quando si aggiunge un brano a una playlist join, penso che questo può funzionare:

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) 

Credo playlist_names e song_names sono ora mappato dal loro indice in questa modo. Come in: nome_elenco_script [0] il nome del primo brano è nome_pelle [0] e nome nome brano primo nome_listlist [1] è nome brano [1] e così via. Sono sicuro che potresti combinarli in un hash o in un array molto facilmente con i metodi integrati in ruby.

Mi rendo conto che stavate cercando un modo efficace per farlo, e avete detto nei commenti che non volevate usare un blocco, e non sono sicuro se per efficienza intendeste una query all-in-one. Mi sto solo abituando a combinare tutti questi metodi di ricerca dei binari e forse guardando ciò che ho qui, puoi modificare le cose secondo le tue esigenze e renderle più efficienti o più condensate.

Spero che questo aiuti.