web-dev-qa-db-fra.com

ActiveRecord.find (array_of_ids), en 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 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?

89
Leonid Shevtsov

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

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.

74
Gunchars

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

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 ?) ...

18
gingerlime

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.

17
JacobEvelyn

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

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

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

1
Mike Woodhouse

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

1
Christian Fazzini

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.

1
XML Slayer

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

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

0
Sam Kah Chiin

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