web-dev-qa-db-fra.com

Rechercher les enregistrements de modèle par ID dans l'ordre indiqué au tableau d'identifiants

J'ai une requête pour obtenir les identifiants des personnes dans un ordre particulier, par exemple: ids = [1, 3, 5, 9, 6, 2]

Je veux ensuite aller chercher ces personnes par Person.find(ids)

Mais ils sont toujours récupérés dans l'ordre numérique, je le sais en effectuant:

people = Person.find(ids).map(&:id)
 => [1, 2, 3, 5, 6, 9]

Comment puis-je exécuter cette requête afin que l'ordre soit le même que celui du tableau ids? 

J'ai rendu la tâche plus difficile car je ne voulais exécuter la requête que pour récupérer les personnes une fois, à partir des ID fournis. Donc, effectuer plusieurs requêtes est hors de question.

J'ai essayé quelque chose comme:

ids.each do |i|
  person = people.where('id = ?', i)

Mais je ne pense pas que cela fonctionne.

33
Jonathan

Note sur ce code:

ids.each do |i|
  person = people.where('id = ?', i)

Il y a deux problèmes avec cela:

Premièrement, la méthode #each renvoie le tableau sur lequel elle a été itérée, vous récupérerez donc simplement les identifiants. Ce que vous voulez, c'est une collecte

Deuxièmement, où retournera un objet Arel :: Relation, qui sera finalement évalué comme un tableau. Donc, vous vous retrouveriez avec un tableau de tableaux. Vous pouvez résoudre deux problèmes.

La première façon serait en aplatissant:

ids.collect {|i| Person.where('id => ?', i) }.flatten

Encore meilleure version:

ids.collect {|i| Person.where(:id => i) }.flatten

Une seconde façon serait simplement de faire une découverte:

ids.collect {|i| Person.find(i) }

C'est gentil et simple

Vous constaterez cependant que tous effectuent une requête à chaque itération, ce qui n’est donc pas très efficace.

J'aime la solution de Sergio, mais voici une autre solution que j'aurais suggérée:

people_by_id = Person.find(ids).index_by(&:id) # Gives you a hash indexed by ID
ids.collect {|id| people_by_id[id] }

Je jure que je me souviens qu'ActiveRecord faisait cet ordre pour nous. Peut-être est-il parti avec Arel;)

30
Brian Underwood

Il y a deux façons d'obtenir des entrées en fonction d'un tableau d'identifiants. Si vous travaillez sur Rails 4, les méthodes dynamiques sont obsolètes, vous devez vous reporter à la solution spécifique à Rails 4 ci-dessous. 

Solution one:

Person.find([1,2,3,4])

Ceci augmentera ActiveRecord::RecordNotFound si aucun enregistrement n'existe

Solution deux [Rails 3 uniquement]:

Person.find_all_by_id([1,2,3,4])

Cela ne causera pas d'exception, retourne simplement un tableau vide si aucun enregistrement ne correspond à votre requête.

En fonction de vos besoins, choisissez la méthode que vous souhaitez utiliser ci-dessus, puis triez-les selon ids

ids = [1,2,3,4]
people = Person.find_all_by_id(ids)
# alternatively: people = Person.find(ids)
ordered_people = ids.collect {|id| people.detect {|x| x.id == id}}

Solution [Rails 4 uniquement]:

Je pense que Rails 4 offre une meilleure solution.

# without eager loading
Person.where(id: [1,2,3,4]).order('id DESC')

# with eager loading.
# Note that you can not call deprecated `all`
Person.where(id: [1,2,3,4]).order('id DESC').load
13
activars

À mon avis, vous pouvez mapper les identifiants ou trier le résultat. Pour ces derniers, il existe déjà des solutions, bien que je les trouve inefficaces.

Cartographie des identifiants:

ids = [1, 3, 5, 9, 6, 2]
people_in_order = ids.map { |id| Person.find(id) }

Notez que cela entraînera l'exécution de plusieurs requêtes, ce qui est potentiellement inefficace.

Tri du résultat:

ids = [1, 3, 5, 9, 6, 2]
id_indices = Hash[ids.map.with_index { |id,idx| [id,idx] }] # requires Ruby 1.8.7+
people_in_order = Person.find(ids).sort_by { |person| id_indices[person.id] }

Ou, en développant Brian Underwoods, répondez:

ids = [1, 3, 5, 9, 6, 2]
indexed_people = Person.find(ids).index_by(&:id) # I didn't know this method, TIL :)
people_in_order = indexed_people.values_at(*ids)

J'espère que cela pourra aider

10
apeiros

Si vous avez un tableau ids, il est aussi simple que - Person.where(id: ids).sort_by {|p| ids.index(p.id) } OU 

persons = Hash[ Person.where(id: ids).map {|p| [p.id, p] }] ids.map {|i| persons[i] }

6
Sandip Ransing

Ancienne question, mais le tri peut être effectué en ordonnant à l'aide de la fonction SQL FIELD. (testé uniquement avec MySQL.)

Donc, dans ce cas, quelque chose comme ceci devrait marcher:

Person.order(Person.send(:sanitize_sql_array, ['FIELD(id, ?)', ids])).find(ids)

Le résultat est le SQL suivant:

SELECT * FROM people
  WHERE id IN (1, 3, 5, 9, 6, 2)
  ORDER BY FIELD(id, 1, 3, 5, 9, 6, 2)
5
Koen.

Vous pouvez obtenir les utilisateurs triés par id asc dans la base de données, puis les réorganiser dans l'application comme vous le souhaitez. Regarde ça:

ids = [1, 3, 5, 9, 6, 2]
users = ids.sort.map {|i| {id: i}} # Or User.find(ids) or another query

# users sorted by id asc (from the query)
users # => [{:id=>1}, {:id=>2}, {:id=>3}, {:id=>5}, {:id=>6}, {:id=>9}]

users.sort_by! {|u| ids.index u[:id]} 

# users sorted as you wanted
users # => [{:id=>1}, {:id=>3}, {:id=>5}, {:id=>9}, {:id=>6}, {:id=>2}]

L'astuce consiste à trier le tableau en fonction d'une valeur artificielle: index de l'identifiant de l'objet dans un autre tableau.

4
Sergio Tulentsev

La plupart des autres solutions ne vous permettent pas de filtrer davantage la requête résultante. C'est pourquoi j'aime bien la réponse de Koen

Semblable à cette réponse mais pour Postgres, j’ajoute cette fonction à mon ApplicationRecord (Rails 5+) ou à n’importe quel modèle (Rails 4):

def self.order_by_id_list(id_list)
  values_clause = id_list.each_with_index.map{|id, i| "(#{id}, #{i})"}.join(", ")
  joins("LEFT JOIN (VALUES #{ values_clause }) AS #{ self.table_name}_id_order(id, ordering) ON #{ self.table_name }.id = #{ self.table_name }_id_order.id")
    .order("#{ self.table_name }_id_order.ordering")
end

La solution de requête est de cette question .

2
Jerph

Si vous utilisez MySQL, cela pourrait être une solution simple pour vous.

Post.where(sky: 'blue').order("FIELD(sort_item_field_id, 2,5,1,7,3,4)")
1
Eskim0

Avec Rails 5, j'ai constaté que cette approche fonctionnait (avec postgres au moins), même pour des requêtes étendues, utile pour travailler avec ElasticSearch:

Person.where(country: "France").find([3, 2, 1]).map(&:id)
=> [3, 2, 1]

Notez que l'utilisation de where au lieu de find ne conserve pas l'ordre.

Person.where(country: "France").where([3, 2, 1]).map(&:id)
=> [1, 2, 3]
0
elliotcm