ActiveRecord.find (array_of_ids), préservant l’ordre

Lorsque vous faites Something.find(array_of_ids) dans Rails, l’ordre du tableau résultant ne dépend pas de l’ordre de array_of_ids .

Est-il possible de faire la recherche et de conserver la commande?

ATM Je sortinge manuellement les enregistrements en fonction de l’ordre des ID, mais c’est un peu boiteux.

UPD: s’il est possible de spécifier l’ordre en utilisant le paramètre :order et une sorte de clause SQL, alors comment?

La réponse est pour mysql seulement

Il y a une fonction dans mysql appelée FIELD ()

Voici comment vous pouvez l’utiliser dans .find ():

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

Curieusement, personne n’a suggéré quelque chose comme ceci:

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

Aussi efficace que cela soit en plus de laisser SQL backend le faire.

Edit: Pour améliorer ma propre réponse, vous pouvez aussi le faire comme ceci:

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

#index_by et #slice sont des ajouts pratiques dans ActiveSupport pour les tableaux et les hachages respectivement.

Comme Mike Woodhouse l’a indiqué dans sa réponse , cela se produit parce que Rails utilise une requête SQL avec une WHERE id IN... clause pour récupérer tous les enregistrements d’une requête. C’est plus rapide que de récupérer chaque identifiant individuellement, mais comme vous l’avez remarqué, cela ne préserve pas l’ordre des enregistrements que vous récupérez.

Pour résoudre ce problème, vous pouvez sortinger les enregistrements au niveau de l’application en fonction de la liste originale des ID que vous avez utilisés lors de la recherche de l’enregistrement.

Sur la base des nombreuses réponses excellentes pour sortinger un tableau en fonction des éléments d’un autre tableau , je recommande la solution suivante:

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

Ou si vous avez besoin de quelque chose de plus rapide (mais sans doute un peu moins lisible), vous pouvez le faire:

 Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids) 

Cela semble fonctionner pour postgresql ( source ) – et retourne une relation ActiveRecord

 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]) 

peut être étendu pour d’autres colonnes, par exemple pour la colonne name , utilisez position(name::text in ?)

Comme je l’ai répondu ici , je viens juste de sortir une gem ( order_as_specified ) qui vous permet de faire un ordre SQL natif comme ceci:

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

Pour autant que j’ai pu tester, il fonctionne en mode natif dans tous les RDBMS, et il retourne une relation ActiveRecord qui peut être chaînée.

Impossible en SQL de fonctionner dans tous les cas malheureusement, vous devriez écrire des trouvailles uniques pour chaque enregistrement ou commande dans ruby, bien qu’il y ait probablement un moyen de le faire fonctionner en utilisant des techniques propriétaires:

Premier exemple:

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

TRÈS INEFFICIENT

Deuxième exemple:

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

@Gunchars answer is great, mais cela ne fonctionne pas par défaut dans Rails 2.3 car la classe Hash n’est pas ordonnée. Une solution simple consiste à étendre la classe index_byindex_by pour utiliser la classe OrderedHash:

 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 

Maintenant, l’approche de @Gunchars fonctionnera

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

Prime

 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 

alors

 Something.find_with_relevance(array_of_ids) 

Sous le capot, find avec un tableau d’ WHERE id IN... générera un SELECT avec une WHERE id IN... , qui devrait être plus efficace que de parcourir les identifiants.

Ainsi, la requête est satisfaite en une fois dans la firebase database, mais SELECT sans les clauses ORDER BY sont pas sortingées. ActiveRecord comprend cela, donc nous élargissons notre find comme suit:

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

Si l’ordre des identifiants de votre tableau est arbitraire et significatif (c’est-à-dire que vous voulez que l’ordre des lignes corresponde à votre tableau indépendamment de la séquence des identifiants), je pense que vous seriez le meilleur en post-traitant les résultats. code – vous pouvez créer une clause :order mais ce serait compliqué et pas du tout révélateur d’intention.

Bien que je ne le vois pas mentionné quelque part dans un CHANGELOG, il semble que cette fonctionnalité ait été modifiée avec la version 5.2.0 .

Ici, validez la mise à jour des documents étiquetés 5.2.0 Cependant, il semble que cette version ait également été réinjectée dans la version 5.0 .

En supposant que Model.pluck(:id) renvoie [1,2,3,4] et que vous voulez l’ordre de [2,4,1,3]

Le concept consiste à utiliser la clause SQL ORDER BY CASE WHEN . Par exemple:

 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; 

Dans Rails, vous pouvez y parvenir en utilisant une méthode publique dans votre modèle pour construire une structure similaire:

 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 

Alors fais:

 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 

La bonne chose à ce sujet. Les résultats sont renvoyés sous forme de relations ActiveRecord (vous permettant d'utiliser des méthodes telles que last , count , where , pluck , etc.)

Il y a un joyau find_with_order qui vous permet de le faire efficacement en utilisant une requête SQL native.

Et il supporte à la fois Mysql et PostgreSQL .

Par exemple:

 Something.find_with_order(array_of_ids) 

Si vous voulez une relation:

 Something.where_with_order(:id, array_of_ids) 

Il y a une clause order dans find (: order => ‘…’) qui fait cela lors de l’extraction des enregistrements. Vous pouvez également obtenir de l’aide ici.

texte du lien