Wenn Sie Something.find(array_of_ids)
in Rails ausführen, hängt die Reihenfolge des resultierenden Arrays nicht von der Reihenfolge von array_of_ids
ab.
Gibt es eine Möglichkeit, die Bestellung zu finden und zu erhalten?
ATM Ich sortiere die Datensätze manuell nach der Reihenfolge der IDs, aber das ist irgendwie lahm.
UPD: Wenn es möglich ist, die Reihenfolge mithilfe des Parameters :order
und einer Art SQL-Klausel anzugeben, wie?
Die Antwort gilt 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]
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, neben dem SQL-Backend.
Edit: Um meine Antwort zu verbessern, kannst du es auch so machen:
Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values
#index_by
und #slice
sind ziemlich nützliche Ergänzungen in ActiveSupport für Arrays und Hashes.
Wie in Mike Woodhouse in seiner Antwort angegeben, geschieht dies, weil Rails unter der Haube eine SQL-Abfrage mit einem WHERE id IN... clause
verwendet, um alle Datensätze in einer Abfrage abzurufen. Dies ist schneller als das Abrufen der einzelnen IDs, aber wie Sie festgestellt haben, wird die Reihenfolge der Datensätze, die Sie abrufen, nicht beibehalten.
Um dies zu beheben, können Sie die Datensätze auf Anwendungsebene nach der ursprünglichen Liste der IDs sortieren, die Sie beim Abfragen des Datensatzes verwendet haben.
Aufgrund der vielen hervorragenden Antworten auf Sortiere ein Array 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 möglicherweise etwas weniger lesbares) benötigen, können Sie Folgendes tun:
Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)
Dies scheint für postgresql ( source ) - 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, z. Verwenden Sie für die Spalte name
position(name::text in ?)
...
Als ich hier antwortete, habe ich gerade einen Edelstein ( order_as_specified ) veröffentlicht, mit dem Sie native SQL-Bestellungen wie folgt durchführen können:
Something.find(array_of_ids).order_as_specified(id: array_of_ids)
Soweit ich testen konnte, funktioniert es in allen RDBMSs nativ und gibt eine ActiveRecord-Relation zurück, die verkettet werden kann.
In SQL nicht möglich, was in allen Fällen funktionieren würde, müssen Sie für jeden Datensatz entweder einzelne Suchen oder eine Reihenfolge in Ruby schreiben, obwohl es wahrscheinlich eine Möglichkeit gibt, sie mithilfe proprietärer Techniken zum Laufen zu bringen:
Erstes Beispiel:
sorted = arr.inject([]){|res, val| res << Model.find(val)}
SEHR UNEFFIZIENTE
Zweites Beispiel:
unsorted = Model.find(arr)
sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}}
Die Antwort von @Gunchars ist großartig, aber sie funktioniert in Rails 2.3 nicht sofort, da die Hash-Klasse nicht geordnet ist. Eine einfache Problemumgehung besteht darin, die Enumerable-Klasse index_by
zur Verwendung der OrderedHash-Klasse zu erweitern:
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 funktioniert der Ansatz von @Gunchars
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)
Unter der Haube generiert find
mit einem Array von IDs eine SELECT
mit einer WHERE id IN...
-Klausel, die effizienter sein sollte als das Durchlaufen der IDs.
Die Anforderung wird also in einer einzigen Fahrt zur Datenbank erfüllt, aber SELECT
s ohne ORDER BY
-Klauseln sind nicht sortiert. ActiveRecord versteht das, also erweitern wir unsere find
wie 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 mit Ihrem Array übereinstimmt, unabhängig von der Reihenfolge der IDs, die darin enthalten sind), sind Sie der Meinung, dass Sie der Server am besten durch Nachbearbeitung der Ergebnisse sind Code - Sie könnten eine :order
-Klausel erstellen, die jedoch sehr kompliziert und überhaupt nicht absichtsvoll ist.
Angenommen, Model.pluck(:id)
gibt [1,2,3,4]
zurück und Sie möchten die Reihenfolge von [2,4,1,3]
Das Konzept besteht darin, die ORDER BY CASE WHEN
-SQL-Klausel zu verwenden. Zum Beispiel:
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 in Ihrem Modell eine öffentliche Methode zum Erstellen einer ähnlichen Struktur verwenden:
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 werden als ActiveRecord Relations zurückgegeben (damit können Sie Methoden wie last
, count
, where
, pluck
usw.) verwenden.
Obwohl ich nirgendwo in einem CHANGELOG erwähnt werde, sieht es so aus, als ob diese Funktionalität mit der Version 5.2.0
geändert wurde .
Hier commit Aktualisierung der Dokumente, die mit 5.2.0
markiert sind. Es scheint jedoch auch backported in Version 5.0
zu sein.
Mit Bezug auf die Antwort hier
Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')")
funktioniert für Postgresql.
Es gibt ein gem find_with_order , mit dem Sie es effizient tun können, indem Sie die native SQL-Abfrage verwenden.
Und es unterstützt sowohl Mysql
als auch PostgreSQL
.
Zum Beispiel:
Something.find_with_order(array_of_ids)
Wenn Sie eine Beziehung wünschen:
Something.where_with_order(:id, array_of_ids)