Saubere Art und Weise Active Objekte von id in der Reihenfolge zu finden angegeben
-
03-07-2019 - |
Frage
Ich möchte gegeben eine Reihe von ids eine Reihe von Active Objekte erhalten.
Ich nahm an,
Object.find([5,2,3])
Würde einen Array zurück mit dem Objekt 5, Objekt 2, dann 3-Objekt in dieser Reihenfolge, sondern werde ich ein Array als Objekt 2, Objekt-3 bestellt und dann 5 Objekt.
Die Activebasis Methode API finden erwähnt, dass Sie sollten nicht erwarten, dass es in der angegebenen Reihenfolge (andere Dokumentation gibt nicht diese Warnung).
Eine mögliche Lösung wurde in durch Array von IDs in der gleichen Suche bestellen? , aber die Reihenfolge Option scheint nicht gültig für SQLite zu sein.
kann ich einige Ruby-Code schreiben, um die Objekte selbst (entweder etwas einfach und schlecht Skalierung oder bessere Skalierung und komplexere) zu sortieren, aber gibt es einen besseren Weg?
Lösung
Es ist nicht, dass MySQL und andere DBs Art Dinge auf eigene Faust, es ist, dass sie sortieren sie nicht. Wenn Sie Model.find([5, 2, 3])
aufrufen, erzeugt das SQL ist so etwas wie:
SELECT * FROM models WHERE models.id IN (5, 2, 3)
Dies ist keine Bestellung angeben, nur die Datensätze zurückgegeben werden soll. Es stellt sich heraus, dass in der Regel MySQL die Datenbankzeilen in 'id'
Ordnung zurückkehren, aber es gibt keine Garantie dafür.
Die einzige Möglichkeit, die Datenbank zurück Datensätze in einen garantiert, um zu bekommen, ist eine Bestellung Klausel hinzuzufügen. Wenn Ihre Unterlagen immer in einer bestimmten Reihenfolge zurückgegeben werden, dann können Sie eine Sortierspalte mit dem db hinzufügen und Model.find([5, 2, 3], :order => 'sort_column')
tun. Ist dies nicht der Fall ist, werden Sie die Sortierung in Code zu tun haben:
ids = [5, 2, 3]
records = Model.find(ids)
sorted_records = ids.collect {|id| records.detect {|x| x.id == id}}
Andere Tipps
Basierend auf meinem vorherigen Kommentar zu Jeroen van Dijk können Sie dies effizienter tun und in zwei Linien mit each_with_object
result_hash = Model.find(ids).each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}
hier als Referenz ist der Maßstab i verwendet
ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do
100000.times do
result_hash = results.each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}
end
end.real
#=> 4.45757484436035 seconds
Nun ist die andere
ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do
100000.times do
ids.collect {|id| results.detect {|result| result.id == id}}
end
end.real
# => 6.10875988006592
Aktualisieren
Sie können dies in den meisten Ordnung und Case-Anweisungen verwenden, hier ist eine Klasse, Methode, die Sie verwenden können.
def self.order_by_ids(ids)
order_by = ["case"]
ids.each_with_index.map do |id, index|
order_by << "WHEN id='#{id}' THEN #{index}"
end
order_by << "end"
order(order_by.join(" "))
end
# User.where(:id => [3,2,1]).order_by_ids([3,2,1]).map(&:id)
# #=> [3,2,1]
Offenbar mySQL und andere DB-Management-System Art Dinge auf eigene Faust. Ich denke, dass Sie das tun umgehen kann:
ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
Eine tragbare Lösung wäre, durch eine SQL-CASE-Anweisung in der ORDER zu verwenden. Sie können so ziemlich jeden Ausdruck in einer ORDER verwenden und ein CASE kann als Inline-Lookup-Tabelle verwendet werden. Zum Beispiel, wie dies die SQL sind Sie nach aussehen:
select ...
order by
case id
when 5 then 0
when 2 then 1
when 3 then 2
end
Das ist ziemlich einfach, mit einem wenig von Ruby zu generieren:
ids = [5, 2, 3]
order = 'case id ' + (0 .. ids.length).map { |i| "when #{ids[i]} then #{i}" }.join(' ') + ' end'
Die oben geht davon aus, dass Sie mit Zahlen oder einigen anderen sicheren Werten in ids
arbeiten; wenn das nicht der Fall ist, dann würden Sie verwenden möchten connection.quote
oder einer der Active SQL sanitizer Methoden Ihren ids
richtig zitieren.
Dann die order
Zeichenfolge als Ihre Bestellung Bedingung verwenden:
Object.find(ids, :order => order)
oder in der modernen Welt:
Object.where(:id => ids).order(order)
Dies ist ein bisschen ausführlicher, aber es sollte das gleiche mit jeder SQL-Datenbank arbeiten, und es ist nicht so schwierig, die Hässlichkeit zu verbergen.
Wie ich hier antwortete, Ich habe gerade ein Juwel ( order_as_specified ), die Sie nativen SQL-Bestellung wie dies zu tun erlaubt:
Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])
Just getestet und es funktioniert in SQLite.
Justin Weiss schrieb ein Blog-Artikel über dieses Problem nur zwei Tage.
Es scheint ein guter Ansatz, um die Datenbank über die bevorzugte Reihenfolge zu erzählen, und alle Datensätze in dieser Reihenfolge direkt aus der Datenbank sortierten laden. Beispiel aus seinem Blog-Artikel :
# in config/initializers/find_by_ordered_ids.rb
module FindByOrderedIdsActiveRecordExtension
extend ActiveSupport::Concern
module ClassMethods
def find_ordered(ids)
order_clause = "CASE id "
ids.each_with_index do |id, index|
order_clause << "WHEN #{id} THEN #{index} "
end
order_clause << "ELSE #{ids.length} END"
where(id: ids).order(order_clause)
end
end
end
ActiveRecord::Base.include(FindByOrderedIdsActiveRecordExtension)
Das erlaubt Ihnen zu schreiben:
Object.find_ordered([2, 1, 3]) # => [2, 1, 3]
Hier ist ein performanter (Hash-Lookup, nicht O (n) Array Suche nach erkennen!) Einzeiler, als Methode:
def find_ordered(model, ids)
model.find(ids).map{|o| [o.id, o]}.to_h.values_at(*ids)
end
# We get:
ids = [3, 3, 2, 1, 3]
Model.find(ids).map(:id) == [1, 2, 3]
find_ordered(Model, ids).map(:id) == ids
Weitere (wahrscheinlich effizienter), wie es in Ruby zu tun:
ids = [5, 2, 3]
records_by_id = Model.find(ids).inject({}) do |result, record|
result[record.id] = record
result
end
sorted_records = ids.map {|id| records_by_id[id] }
Hier ist das einfachste, was ich tun konnte mit:
ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
@things = [5,2,3].map{|id| Object.find(id)}
Dies ist wahrscheinlich der einfachste Weg, vorausgesetzt, Sie nicht zu tun haben viele Objekte zu finden, da es eine Reise in die Datenbank für jede ID erforderlich ist.