2015-08-10 11 views
12

Dies ist ein Problem, das ich häufig stolpere. Es gab einige ähnliche Fragen zu diesem Problem, aber keine von ihnen war sehr vollständig (Und sie könnten möglicherweise veraltet sein, da Rails 4 neue Funktionen eingeführt haben könnte, die bei diesem Problem helfen)Sortieren nach Anzahl auf has_many Beziehung

Lassen Sie mich ein einfaches Beispiel für die Problem und die bekannten Wege zu ‚lösen‘ das Problem:


Sagen wir, ich habe ein User Modell und ein Post Modell und ein User has_many :posts

Jetzt habe ich eine Top-fünf der Benutzer erhalten möchten mit die meisten Beiträge.

Nachfolgend sind Optionen, die ich kenne, aber sie alle haben ihre eigenen Nachteile:

1)

users = User.all 
@top_users = users.sort {|a,b| a.posts.count <=> b.posts.count}.take(5) 

Nachteile: Eine Datenbank Anfrage für jeden Benutzer vorgenommen wird, so dass diese Lösung sehr langsam.

2) Verwenden Sie die SQL-Code direkt mit einem Mitglied wird (siehe zum Beispiel this question and answer)

select('users.*, COUNT(posts.id) AS posts_count').joins(:posts).group('users.id').order('posts_count DESC').take(5) 

Dies läuft alle Sortierlogik in der Datenbank. Allerdings:

  • Wir verwenden eine Menge DB-spezifischen Code (In PostgreSQL zum Beispiel würden wir andere Syntax benötigen). Es wäre besser, wenn möglich, ActiveRecord-Methoden zu verwenden.
  • Wenn Sie einen internen Join verwenden, werden Benutzer ohne Posts nie zurückgegeben. Dies ist ein Problem, wenn wir Benutzer auch ohne Posts zurückgeben möchten.

3) Verwenden Sie SQL direkt mit einem OUTER JOIN (siehe zum Beispiel this question and answers)

User.select("users.*, COUNT(posts.id) as posts_count").joins("LEFT OUTER JOIN posts ON posts.user_id = users.id").group("posts.id").order("posts_count DESC") 

Dies auch Benutzer ohne Pfosten zurück. Nachteile:

  • Noch mehr DB-spezifischen Code als # 2, und noch schwerer zu lesen. Verwenden

4) einen Zähler Cache Spalte (Für eine vollständige Erklärung dieser Technik finden this Railscasts episode)

im Grunde erstellen Sie eine neue Spalte auf den User, die für die Titel der aktuellen Zählung von posts hält Benutzer, indem Sie den Wert im Feld jedes Mal ändern, wenn ein neuer Beitrag erstellt oder gelöscht wird.

Dies ist sehr schnell und lesbar. Der Nachteil ist, dass wir dies nur verwenden können, nachdem wir ein neues Feld auf User definiert haben. In vielen Situationen ist dies akzeptabel, aber es wird schwieriger, flexibel zu sein, da die Benutzertabelle geändert werden muss, damit dies pro Assoziation funktioniert, für die wir unter Umständen eine Top-Fünf erstellen möchten. Da es sich um ein zwischengespeichertes Feld handelt, gibt es Datenbankmanipulationen, die keine Aktualisierung des Feldes auslösen.

Gibt es einen schöneren (lesbaren und effizienten) Weg, dies zu erreichen? Vorzugsweise etwas, das integrierte ActiveRecord-Methoden verwendet.

+0

Es gibt eine andere Option, um Ihre Liste mit Nachteilen hinzuzufügen. Sie können dem 'User'-Modell ein Feld' posts_count' hinzufügen. Wenn Posts seltener hinzugefügt werden als die Top 5 der ausgewählten Benutzer, kann dies ein guter Versuch sein. – Glupo

+0

'In PostgreSQL zum Beispiel würden wir andere Syntax brauchen '- was genau ist DB spezifisch in Beispiel # 2? – EugZol

+0

Anstelle eines expliziten 'LEFT OUTER JOIN' könnten Sie versuchen,' includes (...) 'zu verwenden, was implizit einen' OUTER JOIN' bewirkt. Außerdem scheinen Ihre Abfragen in den Optionen 2 und 3 generisch genug zu sein, dass die Syntax in den SQL-Implementierungen, an denen Sie interessiert sind, üblich ist. – lurker

Antwort

1

Hier ist eine Methode, einen Blick wert:

User.joins("left join posts on posts.user_id = users.id"). 
    group(:id). 
    order("count(*) desc"). 
    limit(5) 

Es ist ein bisschen Handbuch im beitreten, aber wenn Sie mindestens fünf Benutzer wussten, dass eine Stelle hatte, oder wollte keine Benutzer aufzulisten, die nicht einen Beitrag haben, verwenden Sie könnten dann regelmäßig beitreten:

User.joins(:posts). 
    group(:id). 
    order("count(*) desc"). 
    limit(5) 

Der Graf (*) ist nicht unbedingt robust, wenn Sie andere hatte has_many dort schließt sich, aber in diesem Fall würden Sie wahrscheinlich generieren möchten eine Abfrage wie zum Beispiel:

select ... 
from users ... 
order by (select count(*) from posts where posts.user_id = users.id) 

p.s. Getestet auf PostgreSQL. Die GROUP BY in der ID-Spalte würde bei Oracle sicherlich nicht funktionieren, bei anderen nicht.

1

Diese Option könnte sich lohnen, sie zu testen, sie könnte nicht getestet werden.

class Post < ActiveRecord::Base 
    belongs_to :user, counter_cache: true 
end 

Verwenden Sie counter_cache und Sie werden eine Tabelle in Ihrem db treffen. in Ihrem db

class User < ActiveRecord::Base 
    has_many :posts 

    def self.top_5 
    order('post_counts DESC').limit(5) 
    end 
end 

posts_count Integer-Spalte in Tabelle Benutzer hinzufügen mit Standard 0.

class AddPostsCountToUsers < ActiveRecord::Migration 
    def change 
    add_column :users, :posts_count, :integer, default: 0 
    end 
end 

Wenn Sie bereits vorhandene Benutzer haben.

Sie müssen die folgenden in Ihrer Konsole, oder machen, dass in einem Rake Aufgabe ausgeführt werden soll, wenn Sie es ein paar Mal laufen müssen:

User.find_each { |user| User.reset_counters(user.id, :posts) } 
+0

Ist das anders als Methode # 4? – Qqwy

0

Sie mögen unter Alsó-

tun können
User.joins(:posts).select('users.*, count(*) as posts_count').group('users.id').order('posts_count') 
5

eine andere Methode, mit einigen Einschränkungen, könnte es Lösung eher ein Teil machen:

User.where(:id => Post.group(:user_id). 
         order("count(*) desc"). 
         limit(5). 
         keys) 

Dies würde in Datenbankbegriffen äußerst effizient sein, um die fünf Benutzer mit der höchsten Anzahl von Posts zu finden, da sie nur einen Index für die user_id-Spalten der posts-Tabelle scannen müssen, also gut für sehr große Datensätze. Es ist auch ziemlich "sauber" Rails/ActiveRecord-Code, der praktisch datenbankunabhängig sein sollte.

Wenn die Benutzer in ihrer Post-Count-Reihenfolge kritisch sind, könnte eine weniger effiziente Sortiermethode verwendet werden, wenn diese fünf identifiziert wurden, oder die Abrufreihenfolge der Schlüssel könnte in Ruby zum Sortieren der zurückgegebenen Benutzer verwendet werden.

+0

Ich mag diesen Ansatz wirklich! :-) 'count (*)' ist neu für mich und scheint sehr schlau für diese Situation zu sein. – Qqwy

+2

Nun, es ist sicherlich effizient, aber es ist inkompatibel mit der Anwendung von Bedingungen auf das Benutzermodell (wie aktiv/inaktiv) und mit der Verwendung von mehreren Bestellbedingungen (zB Reihenfolge nach Anzahl der Beiträge, dann Anzahl der Kommentare), so ist es nicht für jede Situation. –

+0

Warum gibt es einen .count-Aufruf in der Kette? Wird es nicht nur einen einzelnen Wert zurückgeben, der nicht für .keys gilt? –

Verwandte Themen