ActiveRecord.find (array_of_ids) unter Beibehaltung der Reihenfolge

98

Wenn Sie dies Something.find(array_of_ids)in Rails tun , hängt die Reihenfolge des resultierenden Arrays nicht von der Reihenfolge von ab array_of_ids.

Gibt es eine Möglichkeit, die Bestellung zu finden und zu bewahren?

Geldautomat Ich sortiere die Datensätze manuell nach der Reihenfolge der IDs, aber das ist irgendwie lahm.

UPD: Wenn es möglich ist, die Reihenfolge mit dem :orderParameter und einer Art SQL-Klausel anzugeben , wie dann?

Leonid Shevtsov
quelle

Antworten:

71

Die Antwort ist nur für MySQL

In MySQL gibt es eine Funktion namens FIELD ()

So können Sie es in .find () verwenden:

>> ids = [100, 1, 6]
=> [100, 1, 6]

>> WordDocument.find(ids).collect(&:id)
=> [1, 6, 100]

>> WordDocument.find(ids, :order => "field(id, #{ids.join(',')})")
=> [100, 1, 6]

For new Version
>> WordDocument.where(id: ids).order("field(id, #{ids.join ','})")

Update: Dies wird im Rails 6.1 Rails-Quellcode entfernt

Kovyrin
quelle
Kennen Sie das Äquivalent von FIELDS()in Postgres?
Trung Lê
3
Ich schrieb eine plpgsql-Funktion, um dies in postgres - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Omar Qureshi
25
Das funktioniert nicht mehr. Für neuere Rails:Object.where(id: ids).order("field(id, #{ids.join ','})")
Mahemoff
2
Dies ist eine bessere Lösung als die von Gunchars, da sie die Paginierung nicht unterbricht.
pguardiario
.ids funktioniert gut für mich und ist ziemlich schnell Activerecord-Dokumentation
TheJKFever
79

Seltsamerweise hat niemand so etwas vorgeschlagen:

index = Something.find(array_of_ids).group_by(&:id)
array_of_ids.map { |i| index[i].first }

So effizient wie es nur geht, abgesehen davon, dass das SQL-Backend dies tun kann.

Bearbeiten: Um meine eigene Antwort zu verbessern, können Sie dies auch folgendermaßen tun:

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

#index_byund #slicesind ziemlich praktische Ergänzungen in ActiveSupport für Arrays bzw. Hashes.

Gunchars
quelle
Ihre Bearbeitung scheint also zu funktionieren, aber es macht mich nervös. Die Reihenfolge der Schlüssel in einem Hash ist nicht garantiert, oder? Wenn Sie also Slice aufrufen und den Hash "neu geordnet" zurückbekommen, hängt dies wirklich von den Hash-Rückgabewerten in der Reihenfolge ab, in der die Schlüssel hinzugefügt wurden. Dies hängt von einem Implementierungsdetail ab, das sich ändern kann.
Jon
2
@ Jon, die Reihenfolge ist in Ruby 1.9 und jeder anderen Implementierung, die versucht, ihr zu folgen, garantiert. Für 1.8 patcht Rails (ActiveSupport) die Hash-Klasse, damit sie sich genauso verhält. Wenn Sie also Rails verwenden, sollten Sie in Ordnung sein.
Gunchars
danke für die klarstellung, habe das gerade in der dokumentation gefunden.
Jon
13
Das Problem dabei ist, dass es eher ein Array als eine Beziehung zurückgibt.
Velizar Hristov
3
Großartig, aber der Einzeiler funktioniert nicht für mich (Rails 4.1)
Besi
44

Wie Mike Woodhouse in seiner Antwort feststellte , geschieht dies, weil Rails unter der Haube eine SQL-Abfrage mit a verwendet WHERE id IN... clause, um alle Datensätze in einer Abfrage abzurufen. Dies ist schneller als das Abrufen jeder einzelnen ID, aber wie Sie bemerkt haben, wird die Reihenfolge der Datensätze, die Sie abrufen, nicht beibehalten.

Um dies zu beheben, können Sie die Datensätze auf Anwendungsebene gemäß der ursprünglichen Liste der IDs sortieren, die Sie beim Nachschlagen des Datensatzes verwendet haben.

Aufgrund der vielen hervorragenden Antworten zum Sortieren eines Arrays nach den Elementen eines anderen Arrays empfehle ich die folgende Lösung:

Something.find(array_of_ids).sort_by{|thing| array_of_ids.index thing.id}

Oder wenn Sie etwas schnelleres (aber wohl etwas weniger lesbares) benötigen, können Sie dies tun:

Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)
Ajedi32
quelle
3
Die zweite Lösung (mit index_by) scheint für mich zu scheitern und alle Null-Ergebnisse zu liefern.
Ben Wheeler
22

Dies scheint für postgresql ( Quelle ) zu funktionieren - und gibt eine ActiveRecord-Beziehung zurück

class Something < ActiveRecrd::Base

  scope :for_ids_with_order, ->(ids) {
    order = sanitize_sql_array(
      ["position((',' || id::text || ',') in ?)", ids.join(',') + ',']
    )
    where(:id => ids).order(order)
  }    
end

# usage:
Something.for_ids_with_order([1, 3, 2])

kann auch für andere Spalten erweitert werden, zB für die nameSpalte, verwenden Sie position(name::text in ?)...

Ingwerlime
quelle
Du bist mein Held der Woche. Danke dir!
Ntdb
4
Beachten Sie, dass dies nur in trivialen Fällen funktioniert. Sie werden schließlich auf eine Situation stoßen, in der Ihre ID in anderen IDs in der Liste enthalten ist (z. B. wird 1 von 11 gefunden). Eine Möglichkeit, dies zu umgehen, besteht darin, die Kommas in die Positionsprüfung einzufügen und dem Join dann ein endgültiges Komma hinzuzufügen: order = sanitize_sql_array (["position (',' || clients.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy
Guter Punkt, @IrishDubGuy! Ich werde meine Antwort basierend auf Ihrem Vorschlag aktualisieren. Vielen Dank!
gingerlime
Für mich funktioniert Verketten nicht. Hier sollte der Name der Tabelle vor der ID hinzugefügt werden: Text wie dieser: ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] Vollversion, die für mich funktioniert hat: scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) }danke @gingerlime @IrishDubGuy
user1136228
Ich denke, Sie müssen den Tabellennamen hinzufügen, wenn Sie einige Verknüpfungen durchführen ... Dies ist bei ActiveRecord-Bereichen beim Beitritt durchaus üblich.
Ingwerlime
19

Als ich hier antwortete , habe ich gerade ein Juwel ( order_as_specified ) veröffentlicht, mit dem Sie native SQL-Bestellungen wie folgt ausführen können:

Something.find(array_of_ids).order_as_specified(id: array_of_ids)

Soweit ich testen konnte, funktioniert es nativ in allen RDBMS und gibt eine ActiveRecord-Beziehung zurück, die verkettet werden kann.

JacobEvelyn
quelle
1
Alter, du bist so großartig. Danke dir!
Swrobel
5

In SQL ist dies leider nicht in allen Fällen möglich. Sie müssten entweder einzelne Fundstücke für jeden Datensatz schreiben oder in Ruby bestellen, obwohl es wahrscheinlich eine Möglichkeit gibt, ihn mit proprietären Techniken zum Laufen zu bringen:

Erstes Beispiel:

sorted = arr.inject([]){|res, val| res << Model.find(val)}

Sehr ineffizient

Zweites Beispiel:

unsorted = Model.find(arr)
sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}}
Omar Qureshi
quelle
Obwohl nicht sehr effizient, stimme ich zu, dass diese Umgehung DB-unabhängig und akzeptabel ist, wenn Sie eine kleine Anzahl von Zeilen haben.
Trung Lê
Verwenden Sie nicht injizieren, es ist eine Karte:sorted = arr.map { |val| Model.find(val) }
tokland
Der erste ist langsam. Ich stimme dem zweiten mit einer Karte wie dieser zu:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
Kuboon
2

Die Antwort von @Gunchars ist großartig, funktioniert jedoch in Rails 2.3 nicht sofort, da die Hash-Klasse nicht bestellt ist. Eine einfache Problemumgehung besteht darin, die Enumerable-Klasse zu erweitern, um die OrderedHash-Klasse index_byzu verwenden:

module Enumerable
  def index_by_with_ordered_hash
    inject(ActiveSupport::OrderedHash.new) do |accum, elem|
      accum[yield(elem)] = elem
      accum
    end
  end
  alias_method_chain :index_by, :ordered_hash
end

Jetzt wird der Ansatz von @Gunchars funktionieren

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

Bonus

module ActiveRecord
  class Base
    def self.find_with_relevance(array_of_ids)
      array_of_ids = Array(array_of_ids) unless array_of_ids.is_a?(Array)
      self.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values
    end
  end
end

Dann

Something.find_with_relevance(array_of_ids)
Chris Bloom
quelle
2

Angenommen, Sie Model.pluck(:id)kehren zurück [1,2,3,4]und möchten die Reihenfolge von[2,4,1,3]

Das Konzept besteht darin, die ORDER BY CASE WHENSQL-Klausel zu verwenden. Beispielsweise:

SELECT * FROM colors
  ORDER BY
  CASE
    WHEN code='blue' THEN 1
    WHEN code='yellow' THEN 2
    WHEN code='green' THEN 3
    WHEN code='red' THEN 4
    ELSE 5
  END, name;

In Rails können Sie dies erreichen, indem Sie eine öffentliche Methode in Ihrem Modell haben, um eine ähnliche Struktur zu erstellen:

def self.order_by_ids(ids)
  if ids.present?
    order_by = ["CASE"]
    ids.each_with_index do |id, index|
      order_by << "WHEN id='#{id}' THEN #{index}"
    end
    order_by << "END"
    order(order_by.join(" "))
  end
else
  all # If no ids, just return all
end

Dann mach:

ordered_by_ids = [2,4,1,3]

results = Model.where(id: ordered_by_ids).order_by_ids(ordered_by_ids)

results.class # Model::ActiveRecord_Relation < ActiveRecord::Relation

Das Gute daran. Ergebnisse zurückgegeben werden als Active Relations (so dass Sie Methoden verwenden , wie last, count, where, pluck, usw.)

Christian Fazzini
quelle
2

Es gibt ein Juwel find_with_order , mit dem Sie es mithilfe der nativen SQL-Abfrage effizient ausführen können.

Und es unterstützt sowohl Mysqlund PostgreSQL.

Beispielsweise:

Something.find_with_order(array_of_ids)

Wenn Sie eine Beziehung wünschen:

Something.where_with_order(:id, array_of_ids)
Khiav Reoy
quelle
1

Unter der Haube wird findmit einem Array von IDs SELECTeine WHERE id IN...Klausel mit einer Klausel generiert , die effizienter sein sollte als das Durchlaufen der IDs.

Die Anfrage wird also in einer Fahrt in die Datenbank erfüllt, aber SELECTs ohne ORDER BYKlauseln sind unsortiert. ActiveRecord versteht dies und erweitert unser Angebot findwie folgt:

Something.find(array_of_ids, :order => 'id')

Wenn die Reihenfolge der IDs in Ihrem Array willkürlich und signifikant ist (dh Sie möchten, dass die Reihenfolge der zurückgegebenen Zeilen unabhängig von der darin enthaltenen Reihenfolge der IDs mit Ihrem Array übereinstimmt), sind Sie meiner Meinung nach der beste Server, wenn Sie die Ergebnisse in nachbearbeiten Code - Sie könnten eine :orderKlausel erstellen , aber sie wäre teuflisch kompliziert und überhaupt nicht aufschlussreich.

Mike Woodhouse
quelle
Beachten Sie, dass der Options-Hash veraltet ist. (zweites Argument in diesem Beispiel :order => id)
ocodo
1

Obwohl ich es nirgendwo in einem CHANGELOG erwähnt sehe, sieht es so aus, als ob diese Funktionalität mit der Veröffentlichung der Version geändert wurde5.2.0 .

Hier verpflichten Sie sich, die mit getaggten Dokumente zu aktualisieren. 5.2.0Es scheint jedoch auch in die Version zurückportiert worden zu sein 5.0.

XML Slayer
quelle
0

Mit Bezug auf die Antwort hier

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") arbeitet für Postgresql.

Sam Kah Chiin
quelle
-4

In find (: order => '...') gibt es eine order-Klausel, die dies beim Abrufen von Datensätzen tut. Hier können Sie auch Hilfe bekommen.

Link Text

Ashish Jain
quelle