Sortowanie

Witam,
Mam następujące dwa modele:

Ticket: user : string Message: ticket_id : integer content : text from_client : boolean created_at : timestamp
Potrzebuję osiągnąć efekt taki, że po zapytaniu otrzymam listę Ticketów, posortowaną według daty najnowszej wiadomości, a tickety gdzie najnowsza wiadomość jest od klienta będą na górze listy.
Teraz robię to w taki sposób:
Ticket.all(:include => :messages, :order => ‘messages.from_client ASC, messages.created_at DESC’)
Niestety nie działa to poprawnie, bo wiadomości, gdzie ostatnia wiadomość nie jest od klienta lądują czasem na górze, gdzie być nie powinny.

Nie jestem pewien, czy zrozumiale opisałem problem, ale będę wdzięczny za pomoc, bo siedzę nad tym już jakiś czas…

Pozdrawiam,
Yax

[quote=Yax]Ticket.all(:include => :messages, :order => ‘messages.from_client ASC, messages.created_at DESC’)
Niestety nie działa to poprawnie, bo wiadomości, gdzie ostatnia wiadomość nie jest od klienta lądują czasem na górze, gdzie być nie powinny.[/quote]
Prawdopodobnie masz nulle w kolumnie from_client. Ja z reguły robię taki myk, żeby się tym nie martwić (oczywiście jeżeli null w tej kolumnie nie jest potrzebny):

add_column :tickets, :from_client, :boolean, :null => false, :default => false

W Twoim wypadku, jeżeli już te nulle masz, musisz zmienić zapytanie. Napisz jaką masz bazę, bo rozwiązanie od tego zależy - albo od razu pogoogluj o order z nullami dla konkretnej bazy.

Nie ma nulli, przy zapisywaniu ustawiam to zawsze. Baza to MySQL. Problem występuje w sytuacji, gdy tickety mają takie wiadomosci:

[code]Ticket 1:

  1. 15.10 false
  2. 17.10 true
  3. 18.10 false

Ticket 2:

  1. 13.10 false
  2. 16.10 true[/code]
    Kolejność powinna być: Ticket2, Ticket1, a jest na odwrót. Problem wynika z tego, że Ticket1.2 ma wyzsza date niz Ticket2.2, a chodzi o to, żeby właśnie Ticket1 był niżej, bo ostatnia wiadomość nie jest od klienta.

[quote=Yax]Nie ma nulli, przy zapisywaniu ustawiam to zawsze. Baza to MySQL. Problem występuje w sytuacji, gdy tickety mają takie wiadomosci:

[code]Ticket 1:

  1. 15.10 false
  2. 17.10 true
  3. 18.10 false

Ticket 2:

  1. 13.10 false
  2. 16.10 true[/code]
    Kolejność powinna być: Ticket2, Ticket1, a jest na odwrót. Problem wynika z tego, że Ticket1.2 ma wyzsza date niz Ticket2.2, a chodzi o to, żeby właśnie Ticket1 był niżej, bo ostatnia wiadomość nie jest od klienta.[/quote]
    Sorry, kompletnie źle zinterpretowałem Twojego posta :wink:

Spróbuj coś takeigo:

Ticket.all(:select => "DISTINCT tickets.*, max(messages.created_at)"....)

Fajnie by było też jakbyś wkleił jakie zapytanie Ci się generuje, bo nie wiem jak działa z mysql ten :include.

basic_state_order jest zawarty w Ticket, z nim nie ma problemów.

tickets = Ticket.search() #czasem cos sie dzieje tutaj, czasem nie - searchlogic order = 'basic_state_order ASC, `messages`.from_client DESC, `messages`.created_at DESC' @tickets = tickets.all(:include => :messages, :order => order).paginate :page => params[:page]
Ała:

SELECT `tickets`.`id` AS t0_r0, `tickets`.`category_id` AS t0_r1, `tickets`.`employee_name` AS t0_r2, `tickets`.`order_number` AS t0_r3, `tickets`.`email` AS t0_r4, `tickets`.`created_at` AS t0_r5, `tickets`.`updated_at` AS t0_r6, `tickets`.`basic_state` AS t0_r7, `tickets`.`type` AS t0_r8, `tickets`.`state` AS t0_r9, `tickets`.`subject` AS t0_r10, `tickets`.`explanation` AS t0_r11, `tickets`.`basic_state_order` AS t0_r12, `messages`.`id` AS t1_r0, `messages`.`ticket_id` AS t1_r1, `messages`.`content` AS t1_r2, `messages`.`from` AS t1_r3, `messages`.`created_at` AS t1_r4, `messages`.`updated_at` AS t1_r5, `messages`.`from_client` AS t1_r6 FROM `tickets` LEFT OUTER JOIN `messages` ON messages.ticket_id = tickets.id ORDER BY basic_state_order ASC, `messages`.from_client DESC, `messages`.created_at DESC
A z tym maxem, to nie mam pojęcia jak to zastosować. Zaraz coś spróbuję.

PS.
Edytka była, Distinct sprawdzę.
PS2.
Distinct nic nie zmienia w kodzie zapytania.

Dobra, nie chciało mi się joinów pisać, bo myślałem, że uda się prościej, ale chyba się nie uda :wink:

Hint: musisz dodać inner joina, w który wyciągniesz ostatnią opinię i dopiero po tym wybierać tickety. Jestem teraz na zajęciach, więc sam pokombinuj, jak nie wykombinujesz do wieczora, to spróbuję wrzucić.

Swoją drogą, która to wersja railsów? Myślałem, że :include teraz zawsze wyciąga rekordy w drugim zapytaniu.

Najnowsza. 2.3.4.
:include owszem wyciąga rekordy w drugim zapytaniu, ale tylko jeżeli masz prostego include. Jeżeli, tak jak u mnie, korzystasz z kolumn dodatkowych, np.: przy sortowaniu, to generuje takiego joina dużego.
Pokombinuję z joinami, ale nie wiem czy się uda, bo moja znajomość SQL ograniczała się do tej pory z korzystania z finda zwykłego w Railsach, a wcześniej w PHP tylko selecty i inserty ;p

Posiedziałem nad tym chwilę na przerwie i wyszło mi coś takiego:

SELECT * FROM tickets INNER JOIN ( SELECT messages.* FROM messages INNER JOIN ( SELECT * FROM messages ORDER BY created_at DESC ) as last_messages ON last_messages.id = messages.id GROUP BY messages.ticket_id ) as last_ticket_messages ON tickets.id = last_ticket_messages.ticket_id ORDER BY last_ticket_messages.from_client DESC, last_ticket_messages.created_at DESC;
Trochę potworek, ale na nic prostszego na szybko nie wpadłem :wink: Jak kogoś sql-fu jest potężniejsze niż moje to chętnie zobaczę krótsze rozwiązanie :slight_smile:

Przy takim zapytaniu ja bym najchętniej keszował sobie last_message_id w tabeli tickets - nie będziesz musiał się bawić wtedy w takie chore joiny (chyba, że znajdziesz krótszą wersję). Wtedy możesz zrobić sobie nawet:

[code]has_one :last_message, :class_name => “Message”, :foreign_key => “last_message_id”

#i powinno zadziałać
Ticket.all(:join => :last_message, …)[/code]

Select niestety nie działa. Daje dziwne wyniki, które nie istnieją :wink:

Powinno być chyba:

belongs_to :last_message, :class_name => "Message", :foreign_key => "last_message_id"

Bo has_one, mówi, że w tej drugiej klasie jest pole foreign_key. No i takie zapętlenie has_many messages, belongs_to last message wprowadzi chyba ostre zamieszanie. Wolałbym nie ryzykować tego.

Oczywiście z błędem przepisałem - testowałem to na swojej bazie danych z jakimiś istniejącymi tabelami i zmieniłem tylko nazwy pól i tabel. Sprawdziłem jeszcze raz na szybko wrzuconych tabelach z jedną poprawką i działa chyba całkiem nieźle: http://pastie.org/658732

[quote]Powinno być chyba:

belongs_to :last_message, :class_name => "Message", :foreign_key => "last_message_id"

Bo has_one, mówi, że w tej drugiej klasie jest pole foreign_key. No i takie zapętlenie has_many messages, belongs_to last message wprowadzi chyba ostre zamieszanie. Wolałbym nie ryzykować tego.[/quote]
Tak, racja, belongs_to, nie has_one.

Co do reszty, to co tu ryzykować? :slight_smile:

Bardzo często się przecież cache’uje w jakiejś kolumnie liczniki. Po to jest counter cache. Tak samo można cache’ować inne pola. Możesz również dodać pola last_message_created_at i last_message_from_client, ale last_message_id jest elastyczniejszym wyjściem. Jeżeli ktokolwiek inny w ogóle czyta ten temat, to może znajdzie jakieś minusy takiego rozwiązania, ale moim zdaniem nie ma w tym nic złego. Eliminuje skomplikowane zapytanie na rzecz trzymania id ostatniej wiadomości.

A dlaczego w ogóle sądzisz, że to będzie jakieś zamieszanie? :slight_smile: Tzn. czy czujesz, że będą z tym jakiekolwiek problemy?

Chodziło mi o możliwość wpadnięcia w jakąś pętlę, że railsy będą sobie skakać w kółko pomiędzy ticketem, a wiadomością. Ew. że będą możliwe takie stworki: ticket.last_message.ticket. Tudzież co się stanie jeżeli zrobię ticket.delete?
Jeżeli moje obawy są bezpodstawne to skorzystam z tego rozwiązania, bo rzeczywiście wygląda ładnie.

Raczej nie ma takiej możliwości :slight_smile: To jest najnormalniejsza w świecie asocjacja. Pomyśl o tym tak jakbyś nie miał w ogóle has_many :messages - to jest tylko powiedzenie railsom, że jak napiszesz ticket.last_message, to żeby wykonał konkretne zapytanie, nie ma to żadnego związku z messages.

Jeżeli tak napiszesz, to dostaniesz ticket, nie ma z tym żadnych problemów, dokładnie tak samo jakbyś napisał ticket.messages.first.ticket - analogiczna sytuacja :slight_smile:

Jeżeli nie określisz :dependent to nic szczególnego.

Jeżeli masz has_many :messages, :dependent => :destroy, to zniszczą się wszystkie wiadomości bazując na tej asocjacji, ale jeżeli w last_message nic nie zdefiniujesz, to nic się nie stanie.

Ja mam coś takiego zrobione w jednej z moich aplikacji właśnie z tego powodu - pozbycie się takiego skomplikowanego zapytania w zamian za update’owanie jednego więcej rekordu przy zapisie jest dobrą wymianą.

Tak samo mam czasami po kilka asocjacji dla tej samej tabeli. Na przykład user, który posiada artykuły, które mogą być opublikowane (akurat to nie jest z życia wzięte, ale łatwiej na tym wyjaśnić niż przedstawiać o co chodzi w aplikacji, o której mówię).

[code]class User < ActiveRecord::Base
has_many :articles, :dependent => :destroy

has_many :published_articles,
:class_name => “Article”,
:foreign_key => “article_id”,
:conditions => [“published = ?”, true]
end[/code]
Niby bezsensowna konstrukacja, bo równie dobrze mogę zdefiniować metodę na asocjacji articles, albo dać named_scope :published w modelu Article, ale dzięki temu mogę zrobić:

  User.find(:all, :include => :published_articles)

Przy optymalizacjach może to być niezwykle pomocne, jeżeli często gęsto pobieramy userów z opublikowanymi artykułami (wiem, że nie najlepszy przykład, ale rozumiesz na pewno o co chodzi :).

rozumiem, że potrzebujesz jednej zmiennej i zdefiniowanie dwóch osobnych;

@ticket_from_user = Ticket.from_user @other_tickets = Ticket.not_from_user # sort ....
nie jest w tym przypadku opcją …

I tak trzeba to jeszcze posortować po dacie ostatniej wiadomości, samo from_client dużo tutaj nie komplikuje (chyba, że coś przeoczyłem).

… nie wiem jakie są wytyczne. Przekazywanie do widoku dwóch zmiennych ma sens wtedy gdy Tickety od Userów (niezależnie z kiedy) mają priorytet nad pozostałymi …

@gRuby
To nie ticket jest from_client, tylko niektóre jego wiadomości. Trzymanie tego w dwóch zmiennych też średnio pasuje, bo to przechodzi jeszcze przez paginację.

@drogus
Dziękuję za rozwianie wątpliwości.

Jak zaimplementuję rozwiązanie działające to je tutaj wkleję dla potomności :]

[code=“ruby”]#controllers/tickets_controller.rb
order = ‘basic_state_order ASC, messages.from_client DESC, messages.created_at DESC’
@tickets = Ticket.all(:include => :last_message, :order => order).paginate :page => params[:page]

#models/tickets.rb
belongs_to :last_message, :class_name => ‘Message’, :foreign_key => “last_message_id”

#models/message.rb
after_save :set_last_message

def set_last_message
ticket = self.ticket
ticket.last_message_id = self.id
ticket.save
end[/code]
Działa idealnie, dziękuję za pomoc :slight_smile:

[quote=Yax][code=“ruby”]#controllers/tickets_controller.rb
order = ‘basic_state_order ASC, messages.from_client DESC, messages.created_at DESC’
@tickets = Ticket.all(:include => :last_message, :order => order).paginate :page => params[:page]

#models/tickets.rb
belongs_to :last_message, :class_name => ‘Message’, :foreign_key => “last_message_id”

#models/message.rb
after_save :set_last_message

def set_last_message
ticket = self.ticket
ticket.last_message_id = self.id
ticket.save
end[/code]
Działa idealnie, dziękuję za pomoc :)[/quote]
Nie za ma co :slight_smile:

Ale mam jedną uwagę. Jeżeli będziesz chciał pobrać listę ticketów posortowanych po ostatniej wiadomości, ale też zrobić :include => :messages, to zamień :include => :last_message na :joins => :last_message. Wtedy last_message wyciągnie się joinem, a reszta include. Tylko w takim wypadku musisz jeszcze zobaczyć, który join jest stosowany, żeby nie zgubić np. ticketów, które nie mają wiadomości. To tak dla uściślenia :stuck_out_tongue: