2013-03-08 8 views
10

Próbuję dołączyć do pierwszej piosenki z każdej listy odtwarzania do zestawu list odtwarzania i mam dość trudny czas, aby znaleźć skuteczne rozwiązanie.Zdobądź pierwsze skojarzenie od wielu poprzez stowarzyszenie

mam następujące modele:

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 

chciałbym uzyskać to:

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

Mam dość trudny czas znalezienia skutecznego sposobu, aby to zrobić poprzez sprzężenie.

UPDATE * ***

Shane Andrade ma prowadzić mnie we właściwym kierunku, ale nadal nie mogę dostać dokładnie to, co chcę.

To jest tak daleko, jak udało mi się dostać:

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

To daje mi:

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

Jest blisko, ale muszę pierwszy utwór (wg id) Imię.

+0

Jaką macierz listy odtwarzania chcesz dołączyć do każdej piosenki? Wszystkie listy odtwarzania zawierające tę piosenkę? – Leito

+0

Czy próbujesz wyszukać wszystkie listy odtwarzania z podaną nazwą i daną piosenką? Czy chcesz po prostu wydrukować tablicę wszystkich nazw list odtwarzania obok ich pierwszego utworu? –

+0

@rkirtscientist the later. Nazwy playlist obok ich pierwszego utworu – mrabin

Odpowiedz

9

zakładając, że jesteś na 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

To jest dokładnie to, czego szukałem, dziękuję. Nigdy wcześniej nie robiłem nagrody. Czy teraz akceptuję i przekazuję ci, czy ludzie zwykle czekają, aż skończy się czas? – mrabin

2

To, co robisz powyżej z łączeniami, jest tym, co zrobiłbyś, gdybyś chciał znaleźć każdą playlistę z podaną nazwą i daną piosenką. W celu zebrania PLAYLIST_NAME i pierwszy SONG_NAME z każdej listy odtwarzania można to zrobić:

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

ta zwróci tablicę w tej formie [[playlist_name, first song_name],[another_playlist_name, first_song_name]]

+0

To działa, ale spodziewałem się, że będzie jakiś sposób, aby to zrobić poprzez dołączenie. – mrabin

+0

Powyżej byłoby, jak bym to zrobił, ale możesz również użyć [.zip()] (http://www.ruby-doc.org/core-1.9.3/Array.html#method-i-zip), jeśli obie tablice są tej samej długości i we właściwej kolejności. Metoda '.join()' dla tablicy zwraca ciąg znaków utworzony przez przekształcenie każdego elementu tablicy w ciąg znaków, oddzielony tym, co przekazałeś. Metoda [dołączy się] (http://guides.rubyonrails.org/active_record_querying.html#joining-tables) w sposób, w jaki ją używałeś, jest metodą findera do określania klauzul JOIN w SQL. –

+0

Miałem na myśli .joins not .join. Przepraszam za zamieszanie. – mrabin

0

Myślę, że najlepszym sposobem na to jest użycie wewnętrzne zapytanie, aby uzyskać pierwszy element, a następnie dołączyć do niego.

Nietestowane ale jest to podstawowa idea:

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

Potem wrócić z powrotem do stołu songs ponieważ musimy nazwę utworu. Nie jestem pewien, czy szyny są wystarczająco inteligentne, aby wybrać pola song na ostatnim sprzężeniu. Jeśli nie, może być konieczne dołączenie select na końcu, który wybiera playlists.*, songs.* lub coś podobnego.

2

Myślę, że problem ten zostałby poprawiony dzięki bardziej rygorystycznej definicji "pierwszego". Sugeruję dodanie pola position w modelu PlaylistSong.W którym momencie można po prostu zrobić:

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

Wolałbym nie przechowywać pozycji w tabeli PlaylistSong. Definiuję "pierwszą piosenkę" jako utwór o najniższym identyfikatorze. – mrabin

+0

Zapewne Twoi użytkownicy chcieliby zmienić kolejność swoich list odtwarzania. Następnie musisz śledzić pozycję zdefiniowaną przez użytkownika. :) –

+0

Masz rację, w przyszłości może to być prawda, ale w tej chwili listy odtwarzania i utwory są przechowywane tak jak jest, a kolejność się nie zmienia. Potrzebuję rozwiązania, które będzie działało z tym, co mamy teraz. – mrabin

0

Spróbuj:

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 

Takie rozwiązanie ma kilka zalet:

  • ograniczona do 4 select
  • nie ładuje wszystkie playlist_songs - agregacja po stronie db
  • Nie ładuje wszystkich piosenek - filtrowanie według id's na d b strona

Testowana z MySQL.

To nie pokaże pustych list odtwarzania. i nie może być problemy z niektórymi DB kiedy liczyć odtwarzania> 1000

0

tylko pobrać utwór z drugiej strony:

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

jednak, jak @Dave S. zaproponował, „pierwszy” utwór na playliście jest losowa, chyba że wyraźnie określisz zamówienie (kolumnę position lub cokolwiek innego), ponieważ SQL nie gwarantuje kolejności, w której zwracane są rekordy, chyba że jawnie o to poprosisz.

EDIT

Przepraszam, odczytując pytanie. Myślę, że rzeczywiście kolumna pozycji jest konieczna.

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

Jeśli nie chcemy żadnych pozycji kolumny w ogóle, zawsze można spróbować grupy utworów według listy odtwarzania id, ale musisz select("songs.*, playlist_songs.*") i „pierwsza” piosenka nadal jest przypadkowy. Inną opcją jest użycie RANKwindow function, ale nie jest ona obsługiwana przez wszystkie RDBMS (dla wszystkich, które znam, postgres i serwer sql).

0

można utworzyć stowarzyszenie has_one który w efekcie będzie nazywać się pierwszy utwór, który jest skojarzony z listy

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

Następnie wystarczy wykorzystać to skojarzenie.

Playlist.joins(:playlist_cover) 

AKTUALIZACJA: nie widziałem tabeli łączenia.

można użyć opcji :through dla has_one jeśli masz dołączyć stół

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

To nie zadziała, ponieważ nie ma pola playlist_id w modelu piosenki –

+0

przepraszam. moje oczy całkowicie pominęły ten stół. nadal możesz używać 'has_one', ponieważ ma opcję' through', tak jak w mojej zaktualizowanej odpowiedzi. – jvnill

+0

Podoba mi się ten kierunek, ale pojawia się następujący komunikat: ": Przez skojarzenie" Lista odtwarzania # playlist_cover "to: przez skojarzenie" Playlista # playlist_songs "jest zbiorem", co ma sens. A może sugerujesz utworzenie nowej tabeli o nazwie playlist_song_cover? – mrabin

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

nadzieję, że to działa :)

got to:

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

można dodać activerecord scope do swoich modeli, aby zoptymalizować działanie zapytań sql w kontekście aplikacji. Ponadto, zakresy są kompozycyjne, dzięki czemu łatwiej jest uzyskać to, czego szukasz.

Na przykład w modelu Song, możesz chcieć zakres first_song

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

a następnie można zrobić coś takiego

playlists.songs.first_song 

Uwaga, można również dodać kilka zakresów do modelu skojarzenia PlaylistSongs lub do Twojego modelu playlisty.

0

Nie powiem, jeśli miał znaczników czasu w swojej bazie danych. Jeśli nie, choć i rekordy na łączeniu PlaylistSongs tabeli są tworzone podczas dodać utwór do listy odtwarzania, myślę, że to może działać:

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) 

wierzę playlist_names i song_names są teraz odwzorowany przez ich indeksu w tym droga. Jako: nazwa_play_name [0] nazwa pierwszego utworu to nazwa_pieku [0], a nazwa_listy_kanału [1] nazwa pierwszego utworu to nazwa_pieku [1] i tak dalej. Jestem pewien, że można je łączyć w hasz lub tablicę bardzo łatwo z wbudowanymi metodami ruby.

Zdaję sobie sprawę, że szukałeś skutecznego sposobu, aby to zrobić, i powiedziałeś w komentarzach, że nie chcesz używać bloku, i nie jestem pewien, czy efektywnie chodziło ci o kwerendę typu "wszystko w jednym". Po prostu przyzwyczaję się do łączenia wszystkich metod zapytań dotyczących szyn i być może patrząc na to, co tu mam, możesz modyfikować rzeczy zgodnie ze swoimi potrzebami i sprawiać, że są bardziej wydajne lub skondensowane.

Mam nadzieję, że to pomoże.

Powiązane problemy