it-swarm.com.de

ActiveRecord.find (array_of_ids), wobei die Reihenfolge beibehalten wird

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?

89
Leonid Shevtsov

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]
66
kovyrin

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.

74
Gunchars

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)
34
Ajedi32

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 nameposition(name::text in ?) ...

18
gingerlime

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.

17
JacobEvelyn

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}}
5
Omar Qureshi

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)
2
Chris Bloom

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 SELECTs 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.

1
Mike Woodhouse

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.

1

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.

1
XML Slayer

Mit Bezug auf die Antwort hier

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

0
Sam Kah Chiin

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)
0
khiav reoy