Question

Je souhaite obtenir un tableau d'objets ActiveRecord à partir d'un tableau d'identifiants.

J'ai supposé que

Object.find([5,2,3])

Renverrait un tableau d'objet 5, d'objet 2, puis d'objet 3 dans cet ordre, mais je reçois à la place un tableau ordonné en tant qu'objet 2, objet 3, puis objet 5.

L'ActiveRecord Base l'API de méthode de recherche mentionne le fait que vous ne devriez pas attendez-le dans l'ordre indiqué (les autres documents ne donnent pas cet avertissement).

Une solution potentielle a été donnée dans Trouver un tableau d'identifiants identique. order? , mais l'option de commande ne semble pas être valide pour SQLite.

Je peux écrire du code ruby ??pour trier les objets moi-même (soit un peu simple et peu évolutif, soit plus efficace et plus complexe), mais existe-t-il une meilleure façon?

Était-ce utile?

La solution

Ce n’est pas que MySQL et les autres bases de données trient les choses par eux-mêmes, c’est qu’ils ne les trient pas. Lorsque vous appelez Model.find ([5, 2, 3]) , le code SQL généré ressemble à quelque chose comme:

SELECT * FROM models WHERE models.id IN (5, 2, 3)

Ceci ne spécifie pas un ordre, mais uniquement le jeu d'enregistrements que vous voulez renvoyer. Il s’avère que généralement, MySQL renvoie les lignes de la base de données dans l’ordre 'id' , mais rien ne le garantit.

Le seul moyen d'obtenir que la base de données retourne les enregistrements dans un ordre garanti consiste à ajouter une clause order. Si vos enregistrements seront toujours renvoyés dans un ordre particulier, vous pouvez ajouter une colonne de tri à la base de données et faire Model.find ([5, 2, 3],: order = > 'sort_column') . Si ce n'est pas le cas, vous devrez effectuer le tri dans le code:

ids = [5, 2, 3]
records = Model.find(ids)
sorted_records = ids.collect {|id| records.detect {|x| x.id == id}} 

Autres conseils

D'après mon commentaire précédent à Jeroen van Dijk, vous pouvez le faire plus efficacement et sur deux lignes en utilisant each_with_object

result_hash = Model.find(ids).each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}

Pour référence, voici le point de référence que j’ai utilisé

ids = [5,3,1,4,11,13,10]
results = Model.find(ids)

Benchmark.measure do 
  100000.times do 
    result_hash = results.each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
    ids.map {|id| result_hash[id]}
  end
end.real
#=>  4.45757484436035 seconds

Maintenant l'autre

ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do 
  100000.times do 
    ids.collect {|id| results.detect {|result| result.id == id}}
  end
end.real
# => 6.10875988006592

Mettre à jour

Vous pouvez le faire dans la plupart des instructions order et case, voici une méthode de classe que vous pouvez utiliser.

def self.order_by_ids(ids)
  order_by = ["case"]
  ids.each_with_index.map do |id, index|
    order_by << "WHEN id='#{id}' THEN #{index}"
  end
  order_by << "end"
  order(order_by.join(" "))
end

#   User.where(:id => [3,2,1]).order_by_ids([3,2,1]).map(&:id) 
#   #=> [3,2,1]

Apparemment, MySQL et d’autres systèmes de gestion de bases de données trient les choses par eux-mêmes. Je pense que vous pouvez éviter cela en faisant:

ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )

Une solution portable consisterait à utiliser une instruction SQL CASE dans votre ORDER BY. Vous pouvez utiliser à peu près n'importe quelle expression dans un ORDER BY et un CASE peut être utilisé en tant que table de recherche intégrée. Par exemple, le code SQL que vous recherchez ressemble à ceci:

select ...
order by
    case id
    when 5 then 0
    when 2 then 1
    when 3 then 2
    end

C'est assez facile à générer avec un peu de Ruby:

ids = [5, 2, 3]
order = 'case id ' + (0 .. ids.length).map { |i| "when #{ids[i]} then #{i}" }.join(' ') + ' end'

Ce qui précède suppose que vous utilisez des nombres ou d'autres valeurs sûres dans ids ; Si ce n'est pas le cas, vous voudrez utiliser < code> connection.quote ou l'un des Méthodes d'assainisseur SQL ActiveRecord pour citer correctement vos identifiants .

Utilisez ensuite la chaîne order comme condition de commande:

Object.find(ids, :order => order)

ou dans le monde moderne:

Object.where(:id => ids).order(order)

C’est un peu bavard, mais cela devrait fonctionner de la même manière avec toutes les bases de données SQL et il n’est pas si difficile de cacher la laideur.

Comme je répondais ici , je venais de publier une gemme ( order_as_specified ) qui vous permet d'effectuer un ordre SQL natif comme suit:

Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])

Vient de tester et cela fonctionne dans SQLite.

Justin Weiss a écrit un article de blog sur ce problème il y a à peine deux jours.

Il semble être une bonne approche d'indiquer à la base de données l'ordre préféré et de charger tous les enregistrements triés dans cet ordre directement à partir de la base de données. Exemple tiré de son article de blog :

# in config/initializers/find_by_ordered_ids.rb
module FindByOrderedIdsActiveRecordExtension
  extend ActiveSupport::Concern
  module ClassMethods
    def find_ordered(ids)
      order_clause = "CASE id "
      ids.each_with_index do |id, index|
        order_clause << "WHEN #{id} THEN #{index} "
      end
      order_clause << "ELSE #{ids.length} END"
      where(id: ids).order(order_clause)
    end
  end
end

ActiveRecord::Base.include(FindByOrderedIdsActiveRecordExtension)

Cela vous permet d'écrire:

Object.find_ordered([2, 1, 3]) # => [2, 1, 3]

Voici une recherche on-line performante (recherche par hachage, et non pas O (n) comme dans detect!), en tant que méthode:

def find_ordered(model, ids)
  model.find(ids).map{|o| [o.id, o]}.to_h.values_at(*ids)
end

# We get:
ids = [3, 3, 2, 1, 3]
Model.find(ids).map(:id)          == [1, 2, 3]
find_ordered(Model, ids).map(:id) == ids

Une autre façon (probablement plus efficace) de le faire en Ruby:

ids = [5, 2, 3]
records_by_id = Model.find(ids).inject({}) do |result, record| 
  result[record.id] = record
  result
end
sorted_records = ids.map {|id| records_by_id[id] }

Voici la chose la plus simple que je pourrais trouver:

ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
@things = [5,2,3].map{|id| Object.find(id)}

C’est probablement le moyen le plus simple, en supposant que vous n’avez pas trop d’objets à trouver, car il faut se rendre dans la base de données pour chaque identifiant.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top