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 trouver et de préserver l'ordre?
ATM Je trie manuellement les enregistrements en fonction de l'ordre des identifiants, mais c'est un peu boiteux.
UPD: s'il est possible de spécifier l'ordre en utilisant le param :order
et une sorte de clause SQL, alors comment?
La réponse est uniquement pour mysql
Il existe 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 ça:
index = Something.find(array_of_ids).group_by(&:id)
array_of_ids.map { |i| index[i].first }
Aussi efficace que cela puisse être en plus de laisser le backend SQL 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 très pratiques dans ActiveSupport pour les tableaux et les hachages, respectivement.
Comme Mike Woodhouse a déclaré dans sa réponse , cela se produit car, sous le capot, Rails utilise une requête SQL avec un WHERE id IN... clause
pour extraire tous les enregistrements d'une requête. Cela est plus rapide que de récupérer chaque identifiant individuellement, mais comme vous l'avez remarqué, cela ne conserve pas l'ordre des enregistrements que vous récupérez.
Pour résoudre ce problème, vous pouvez trier les enregistrements au niveau de l'application en fonction de la liste d'origine des ID que vous avez utilisée lors de la recherche de l'enregistrement.
Sur la base des nombreuses excellentes réponses à Trier 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 d'un peu plus rapide (mais peut-être 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 renvoie 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 également être étendu à d’autres colonnes, par exemple pour la colonne name
, utilisez position(name::text in ?)
...
En répondant à ici , je viens de publier une gemme ( 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, cela fonctionne de manière native dans tous les SGBDR et renvoie une relation ActiveRecord qui peut être chaînée.
Impossible en SQL de fonctionner dans tous les cas. Malheureusement, vous devez écrire des recherches uniques pour chaque enregistrement ou ordre dans Ruby, bien qu'il existe probablement un moyen de le faire fonctionner avec des techniques propriétaires:
Premier exemple:
sorted = arr.inject([]){|res, val| res << Model.find(val)}
TRES INEFFICIENT
Deuxième exemple:
unsorted = Model.find(arr)
sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}}
La réponse de @Gunchars est excellente, mais cela ne fonctionne pas immédiatement dans Rails 2.3 car la classe Hash n'est pas ordonnée. Une solution de contournement simple consiste à étendre le index_by
de la classe Enumerable 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
Ensuite
Something.find_with_relevance(array_of_ids)
Sous le capot, find
avec un tableau d'identifiants générera un SELECT
avec une clause WHERE id IN...
, ce qui devrait être plus efficace que la boucle entre les identifiants.
Donc, la demande est satisfaite en un voyage dans la base de données, mais les clauses SELECT
s sans ORDER BY
ne sont pas triées. ActiveRecord comprend cela, alors nous développons notre find
comme suit:
Something.find(array_of_ids, :order => 'id')
Si l'ordre des identifiants dans votre tableau est arbitraire et significatif (c'est-à-dire que vous voulez que l'ordre des lignes renvoyé corresponde à votre tableau, quelle que soit la séquence d'identifiants qu'il contient), alors je pense que vous seriez le meilleur serveur en post-traitant code - vous pouvez construire une clause :order
mais ce serait extrêmement compliqué et ne révélerait pas du tout votre intention.
En supposant que Model.pluck(:id)
renvoie [1,2,3,4]
et que vous souhaitiez 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.).
Bien que cela ne soit mentionné nulle part dans un CHANGELOG, il semble que cette fonctionnalité ait été modifiée avec la publication de la version 5.2.0
.
Voici commit mettant à jour les documents étiquetés avec 5.2.0
Cependant, il semble également avoir été backported dans la version 5.0
.
En référence à la réponse ici
Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')")
travaille pour Postgresql.
Il existe une gem find_with_order qui vous permet de le faire efficacement en utilisant une requête SQL native.
Et il prend en charge Mysql
et PostgreSQL
.
Par exemple:
Something.find_with_order(array_of_ids)
Si vous voulez relation:
Something.where_with_order(:id, array_of_ids)