ActiveRecord.find (array_of_ids), préservation de l'ordre

98

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.

Existe-t-il un moyen de rechercher et de conserver la commande?

ATM Je trie manuellement les enregistrements en fonction de l'ordre des ID, mais c'est un peu nul.

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

Léonid Chevtsov
la source

Réponses:

71

La réponse est pour mysql uniquement

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]

For new Version
>> WordDocument.where(id: ids).order("field(id, #{ids.join ','})")

Mise à jour: cela sera supprimé dans le code source de Rails 6.1 Rails

kovyrine
la source
Connaissez-vous l'équivalent de FIELDS()Postgres?
Trung Lê
3
J'ai écrit une fonction plpgsql pour faire cela dans postgres - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Omar Qureshi
25
Cela ne fonctionne plus. Pour les Rails plus récents:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff
2
C'est une meilleure solution que celle de Gunchars car elle ne cassera pas la pagination.
pguardiario
.ids fonctionne très bien pour moi, et est assez rapide activerecord documentation
TheJKFever
79

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 possible, en plus de laisser le backend SQL le faire.

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

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

#index_byet #slicesont des ajouts très pratiques dans ActiveSupport pour les tableaux et les hachages respectivement.

Gunchars
la source
Donc, votre modification semble fonctionner mais cela me rend nerveux. L'ordre des touches dans un hachage n'est pas garanti, n'est-ce pas? Ainsi, lorsque vous appelez slice et récupérez le hachage «réorganisé», cela dépend vraiment du hachage qui renvoie les valeurs dans l'ordre dans lequel ses clés ont été ajoutées. Cela ressemble à un détail d'implémentation qui peut changer.
Jon le
2
@Jon, l'ordre est garanti dans Ruby 1.9 et toutes les autres implémentations qui essaient de le suivre. Pour la version 1.8, Rails (ActiveSupport) corrige la classe Hash pour qu'elle se comporte de la même manière, donc si vous utilisez Rails, vous devriez être d'accord.
Gunchars
merci pour la clarification, je viens de trouver cela dans la documentation.
Jon le
13
Le problème avec ceci est qu'il renvoie un tableau, plutôt qu'une relation.
Velizar Hristov
3
Génial, cependant, le one-liner ne fonctionne pas pour moi (Rails 4.1)
Besi
44

Comme Mike Woodhouse l'a déclaré dans sa réponse , cela se produit parce que, sous le capot, Rails utilise une requête SQL avec un WHERE id IN... clausepour récupérer tous les enregistrements en une seule 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 trier les enregistrements au niveau de l'application en fonction de la liste d'origine des ID que vous avez utilisés lors de la recherche de l'enregistrement.

Sur la base des nombreuses excellentes réponses pour 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 sans doute un peu moins lisible), vous pouvez le faire:

Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)
Ajedi32
la source
3
la deuxième solution (avec index_by) semble échouer pour moi, produisant tous les résultats nuls.
Ben Wheeler
22

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 name colonne, utilisez position(name::text in ?)...

gingembre
la source
Tu es mon héros de la semaine. Je vous remercie!
ntdb
4
Notez que cela ne fonctionne que dans des cas triviaux, vous finirez par vous heurter à une situation où votre identifiant est contenu dans d'autres identifiants de la liste (par exemple, il trouvera 1 sur 11). Une façon de contourner cela est d'ajouter les virgules dans la vérification de position, puis d'ajouter une virgule finale à la jointure, comme ceci: order = sanitize_sql_array (["position (',' || clients.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy
Bon point, @IrishDubGuy! Je mettrai à jour ma réponse en fonction de votre suggestion. Merci!
gingerlime
pour moi, le chaînage ne fonctionne pas. Ici, le nom des tables doit être ajouté avant id: texte comme ceci: ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] version complète qui a fonctionné pour moi: scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } merci @gingerlime @IrishDubGuy
user1136228
Je suppose que vous devez ajouter le nom de la table au cas où vous feriez des jointures ... C'est assez courant avec les étendues ActiveRecord lorsque vous rejoignez.
gingerlime
19

Comme je l'ai répondu ici , je viens de publier un 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)

Autant que j'ai pu tester, cela fonctionne de manière native dans tous les SGBDR, et il renvoie une relation ActiveRecord qui peut être chaînée.

JacobEvelyn
la source
1
Mec, tu es tellement génial. Je vous remercie!
swrobel
5

Pas possible dans SQL qui fonctionnerait dans tous les cas, malheureusement, vous auriez soit besoin d'écrire des recherches uniques pour chaque enregistrement, soit de commander en ruby, bien qu'il existe 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 INEFFICACE

Deuxième exemple:

unsorted = Model.find(arr)
sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}}
Omar Qureshi
la source
Bien que pas très efficace, je conviens que cette solution de contournement est indépendante de la base de données et acceptable si vous avez un petit nombre de lignes.
Trung Lê
sorted = arr.map { |val| Model.find(val) }
N'utilisez pas d'inject
le premier est lent. Je suis d'accord avec le deuxième avec une carte comme celle-ci:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon
2

La réponse @Gunchars est excellente, mais elle ne fonctionne pas directement dans Rails 2.3 car la classe Hash n'est pas ordonnée. Une solution de contournement simple consiste à étendre la classe Enumerable ' index_bypour 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

L'approche de @Gunchars fonctionnera maintenant

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

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

Le concept consiste à utiliser la ORDER BY CASE WHENclause SQL. 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 ayant 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 comme des relations ActiveRecord (vous permettant d'utiliser des méthodes telles que last, count, where, pluck, etc.)

Christian Fazzini
la source
2

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

Et il prend en charge à la fois Mysqlet PostgreSQL.

Par exemple:

Something.find_with_order(array_of_ids)

Si vous voulez une relation:

Something.where_with_order(:id, array_of_ids)
khiav reoy
la source
1

Sous le capot, findavec un tableau d'id générera SELECTune WHERE id IN...clause with a , ce qui devrait être plus efficace que de parcourir les ID.

La requête est donc satisfaite en un seul voyage vers la base de données, mais les SELECTs sans ORDER BYclauses ne sont pas triées. ActiveRecord comprend cela, nous développons donc notre findcomme 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ées corresponde à votre tableau indépendamment de la séquence d'identifiants qu'il contient), je pense que vous seriez le meilleur serveur en post-traitant les résultats dans code - vous pourriez construire une :orderclause mais ce serait diaboliquement compliqué et pas du tout révélateur d'intention.

Mike Woodhouse
la source
Notez que le hachage d'options est obsolète. (deuxième argument, dans cet exemple :order => id)
ocodo
1

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

Ici, commettez la mise à jour des documents étiquetés avec 5.2.0Cependant, il semble avoir également été rétroporté dans la version 5.0.

XML Slayer
la source
0

En référence à la réponse ici

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") fonctionne pour Postgresql.

Sam Kah Chiin
la source
-4

Il y a une clause order dans find (: order => '...') qui fait cela lors de la récupération des enregistrements. Vous pouvez également obtenir de l'aide d'ici.

texte du lien

Ashish Jain
la source