ActiveRecord, niechciany lazyload

Witam,

Jak dotąd moja zabawa z Railsem przebiegała bez większych kłopotów, ale wczoraj natrafiłem na dość specyficzny problem, z którym nie mogę sobie dać rady.

Kod modelu:

[code=ruby]class Link < ActiveRecord::Base
has_one :user_vote
end

class UserVote < ActiveRecord::Base
belongs_to :user
belongs_to :link
end[/code]
Wykonuję taką instrukcję:

@links = Link.find_by_sql( ["SELECT * FROM links LEFT JOIN user_votes ON links.id = user_votes.link_id" + " AND user_votes.user_id = ?", session[:user_id]] )
Ma ona za zadanie pobrać wszystkie linki z tabeli links i jednocześnie sprawdzić, czy użytkownik oddał głos na link (pobierając rekord z tabeli user_votes). Jeśli dany user nie oddał głosu na dany link to MySQL zwraca pusty rekord (NULL). Problem polega na tym, że jeśli powiązanego rekordu nie ma, to przy próbie odwołania się do niego Rails wyciąga go z bazy ponownie. Takie zachowanie nie jest tu pożądane, bo pobrany może zostać głos innego użytkownika.

Moje pytanie brzmi: czy jest jakiś sposób, żeby zablokować lazyload dla has_one?

Do konca nie zrozumialem chyba problemu (nie wiem co tym kodem chcesz uzyskac) ale sprawdz takie rozwiazanie:

@all_links = Link.find(:all, :conditions => ["user_votes.user_id = ?", session[:user_id]], :include => :user_votes)

Twój kod sprawi, że pobrane zostaną tylko te linki, na które użytkownik już oddał głos. Potrzeba mi, żeby pobrane zostały wszystkie.

Ogólnie wygląda to tak: http://www.wykop.pl - mam listę linków i na każdy można oddać swój głos. Problem rodzi się, gdy trzeba sprawdzić, czy użytkownik już oddał swój głos na dany link.

Byc moze nie zrozumialem. Czy to chodzi o funkcjonalnosc typu: pokazuja sie wszystkie linki ale uzytkownik ma tylko mozliwosc zaglosowania na ten na kt. nie oddal juz glosu (button/link disabled itd.)?

Co do find_by_sql + join i sprawy typu find …, :joins => … to nalezy uwazac bo zwraca to obiekt wzbogacony o properties z innej tabeli (np id zostanie nadpisane przez ostatni - teoretycznie nie powinien to byc nawet kind_of? ale taki jest ruby :slight_smile: ). Zreszta z api rails: “The records will be returned read-only since they will have attributes that do not correspond to the table’s columns.” Generalnie uciekania do czystego sqla nalezy unikac chocby ze wzgledu na przenosnosc kodu a z tego co widze jest to w kontrolerze wiec przy duzej aplikacji bedzie nie do wysledzenia.

Rozwiazanie Adamh rzeczywiscie zwroci tylko te na, kt. user oddal glos.

Jezeli ma to dzialac jak zrozumialem to byc moze (nie sprawdzalem):

:include zostaje dla eager fetching

class Link < ActiveRecord::Base
  ...
  attr_accessor: already_voted  #for current user
  ...
end

# i gdziekolwiek
links = Link.find(:all, :include => :user_vote)
links.each { |l| l.already_voted = l.user_vote != nil  and l.user_vote.user_id = current_user_id }

Tak rozwiazanie, ktore podalem robi INNER JOIN i zwraca tylko przeciecie.
Widocznie faktycznie nie zrozumialem
Moze takie rozwiazanie:

[code=ruby]@all_links = Links.find(:all)
@voted_links = Link.find(:all,
:conditions => [“user_votes.user_id = ?”, session[:user_id]],
:include => :user_votes)

a jeszcze ladniej obiektowo (zakladajac, ze relacje sa zrobione dobrze):

@voted_links = User.voted_links
@all_links.each do |link|
#“cokolwiek tam masz robic” if @voted_links.include(link)
end[/code]
Lub takie:

[code=ruby]class Link < ActiveRecord::Base
has_one :user_vote

private
def was_voted_by_user?(user_id)
#zakladam, ze user_vote jest dobrze do usera przypisany
    #tu da sie na pewno zrobic  bardziej wydajne wyszukiwanie
    self.user_vote && self.user_vote.user.id == user_id 
end

end

@all_links = Links.find(:all)
@all_links.each do |link|
“zrob costam” if link.was_voted_by_user?(session[:user_id])
end[/code]
Tu oczywiscie wszystko zalezy co wydajnosciowo jest dla nas lepsze czy chcemy obciazac serwer aplikacji, czy wolimy obciazac serwer bazy.

Mysle, ze podwoje udezanie w baze selectem nie ma sensu wiec 2ie rozwiazanie (*) … ale autor postu niech okresli czy o to chdzilo.

(*) ale dugie wali w baze za kazdym razem? - sorry, ale troche popilem :slight_smile:

Dokładnie.

Hm… Ale jeśli deklaruję has_one, to dane powinny się rozdzielić na obiekty link i user_vote, tak?

pawel: Twój kod nie zadziała, bo Link może mieć wiele głosów (ale tylko 1 danego użytkownika). Jeśli skorzystam z Twojego kodu, to pobrany zostanie losowy rekord, losowego użytkownika. Tym samym nie można wiarygodnie sprawdzić, czy zalogowany już głosował. To samo tyczy się kodu Adama (tego ostatniego).

Natomiast jeśli chodzi o wcześniejsze rozwiązanie Adama, to też mi nie pasuje, bo trzeba byłoby pobrać wszystkie głosy danego użytkownika (a może być ich kilka tysięcy).

Najprościej byłoby zrobić tak:

class Link
has_many :user_votes
end

links = Link.find(:all, :include => :user_votes)

i sprawdzać to w pętli. Nie jest to jednak dobre rozwiązanie, bo pobiera wszystkie głosy dla danego linka (a może być ich np. 100), a mi potrzeba właśnie ten jeden (dla ID zalogowanego usera). Jeśli już zostawać przy has_many, to wyjściem mogłoby być coś takiego:

class Link
has_many :user_votes, :conditions => [“user_id = ?”, session[:user_id]
end

Ale to też z tego, co wiem nie jest możliwe, bo model nie ma dostępu do sesji :confused:

Ogólnie: podane przeze mnie w pierwszym poście zapytanie SQL robi dokładnie to, co chcę osiągnąć. Wszystko psuje rails, bo w przypadku, gdy dla danego linka nie ma powiązanego głosu, próbuje znów wyciągnać go z bazy danych. Skutkuje to zazwyczaj tym, że zostaje pobrany głos innego użytkownika…

Może jest jakiś sposób, żeby nadpisać metodę user_vote, w niej sprawdzić, czy w danych z bazy jest user_vote i uniemożliwić ponowne wykonanie zapytania. Niestety nie mam pojęcia, czy jest to wykonalne. Muszę pogrzebać chyba w kodzie railsa…

class Link
has_one :user_vote

def user_vote
#sprawdz pobrany rekord
if jest_user_vote
return user_vote
else
return nil
end
end

Pawel drugie rozwiazanie przy kazdym odwolaniu odpytuje baze ale to bardzoproste selecty, ktore baza obsluguje bardzo szybko - stad wlasnie te dwa warianty rozwiazania… jedno obciaza zasoby (glownie pamiec) serwera aplikacji a drugie przerzuca prace na baze danych.

Jestem prawie pewien, ze drugi kod jest prawidlowy (nie mam teraz tego jak sprawdzic).

Nie dzieje sie tak jak to opisujesz pierwsza kolekcja zawiera wszystkei linki, a druga linki na ktore dany uzytkownik glosowal. Chyba wlasnie o to chodzilo prawda?

Ale nie dokładnie o to mi chodzi. Mam tylko 25 linków na jednej stronie. Jeżeli użytkownik głosował na 800 linków, a ja potrzebuję sprawdzić tylko 25, to już mało wydajne.

W takim przypadku rowiazanie nr 2 wydaje sie byc bardzo rozsadne.

Rozsadnie chyba byloby zrobic tak:

class Link < ActiveRecord::Base has_many :user_votes end
Wtedy kod obslugujacy bylby troche dluzszy i wydajnosciowo wygladaloby to gorzej.

Uff… chyba rozwiązałem problem :slight_smile:

links = Link.find( :all, :select => "links.*, user_votes.id AS user_has_voted", :joins => "LEFT JOIN user_votes ON links.id = user_votes.link_id AND user_votes.user_id = #{session[:user_id]}" )
A później:

<% unless link.user_has_voted %> <li><%= link_to "głosuj!", :action => "vote", :id => link %></li> <% end %>
Nie wiem, czy da radę inaczej to zrobić. W każdym razie to wydaje się działać poprawnie :slight_smile: Dzięki za wszystkie odpowiedzi.