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?

War es hilfreich?

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.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top