ActiveRecord.find (array_of_ids), preservando l’ordine

Quando Something.find(array_of_ids) in Rails, l’ordine dell’array risultante non dipende dall’ordine di array_of_ids .

C’è un modo per fare la ricerca e preservare l’ordine?

ATM I ordinamento manuale dei record in base all’ordine degli ID, ma è una specie di zoppo.

UPD: se è ansible specificare l’ordine usando il parametro :order e qualche tipo di clausola SQL, allora come?

La risposta è solo per mysql

C’è una funzione in mysql chiamata FIELD ()

Ecco come puoi usarlo in .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] 

Stranamente, nessuno ha suggerito qualcosa del genere:

 index = Something.find(array_of_ids).group_by(&:id) array_of_ids.map { |i| index[i].first } 

Efficace come si ottiene oltre a consentire il back-end SQL farlo.

Modifica: per migliorare la mia risposta, puoi anche farlo in questo modo:

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

#index_by e #slice sono #slice molto utili in ActiveSupport rispettivamente per array e hash.

Come Mike Woodhouse ha affermato nella sua risposta , ciò si verifica perché, sotto la copertura, Rails sta utilizzando una query SQL con una WHERE id IN... clause per recuperare tutti i record in una query. Questo è più veloce del recupero di ciascun id individualmente, ma come hai notato non conserva l’ordine dei record che stai recuperando.

Per risolvere questo problema, è ansible ordinare i record a livello di applicazione in base all’elenco originale degli ID utilizzati durante la ricerca del record.

Sulla base delle numerose risposte eccellenti per ordinare un array in base agli elementi di un altro array , consiglio la seguente soluzione:

 Something.find(array_of_ids).sort_by{|thing| array_of_ids.index thing.id} 

O se hai bisogno di qualcosa un po ‘più veloce (ma probabilmente un po’ meno leggibile) puoi farlo:

 Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids) 

Questo sembra funzionare per postgresql ( fonte ) e restituisce una relazione 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]) 

può essere esteso anche ad altre colonne, ad esempio per la colonna del name , usa position(name::text in ?)

Come ho risposto qui , ho appena rilasciato una gem ( order_as_specified ) che ti permette di fare un ordinamento SQL nativo come questo:

 Something.find(array_of_ids).order_as_specified(id: array_of_ids) 

Per quanto ho potuto testare, funziona in modo nativo in tutti gli RDBMS e restituisce una relazione ActiveRecord che può essere concatenata.

Sfortunatamente in SQL non sarebbe ansible lavorare in tutti i casi, altrimenti dovresti scrivere singoli reperti per ogni record o ordine in ruby, anche se probabilmente c’è un modo per farlo funzionare usando tecniche proprietarie:

Primo esempio:

 sorted = arr.inject([]){|res, val| res << Model.find(val)} 

MOLTO INEFFICIENTE

Secondo esempio:

 unsorted = Model.find(arr) sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}} 

La risposta di @Gunchars è ottima, ma non funziona in Rails 2.3 perché la class di Hash non è ordinata. Una semplice soluzione è estendere la class Enumerable ‘ index_by per utilizzare la class 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 

Ora l’approccio di @Gunchar funzionerà

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

indennità

 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 

Poi

 Something.find_with_relevance(array_of_ids) 

Sotto il cofano, find con una serie di ID genererà un SELECT con una clausola WHERE id IN... , che dovrebbe essere più efficiente del looping degli id.

Quindi la richiesta è soddisfatta in un viaggio nel database, ma SELECT s senza le clausole ORDER BY non sono ordinati. ActiveRecord lo capisce, quindi espandiamo la nostra find come segue:

 Something.find(array_of_ids, :order => 'id') 

Se l’ordine degli id ​​nel tuo array è arbitrario e significativo (cioè vuoi che l’ordine delle righe restituite corrisponda al tuo array indipendentemente dalla sequenza di id in esso contenuta) allora penso che saresti il ​​miglior server dopo aver elaborato i risultati in codice – potresti build una :order clausola di :order , ma sarebbe diabolicamente complicata e per niente intenzionale – rivelatrice.

Anche se non lo vedo menzionato da nessuna parte in un CHANGELOG, sembra che questa funzionalità sia stata cambiata con il rilascio della versione 5.2.0 .

Qui esegui il commit dell’aggiornamento dei documenti taggati con 5.2.0 Tuttavia sembra che sia stato eseguito il backport nella versione 5.0 .

Supponendo Model.pluck(:id) restituisce [1,2,3,4] e vuoi l’ordine di [2,4,1,3]

Il concetto è di utilizzare la clausola ORDER BY CASE WHEN . Per esempio:

 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; 

In Rails, è ansible ottenere ciò avendo un metodo pubblico nel modello per build una struttura simile:

 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 

Quindi fa:

 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 cosa buona di questo. I risultati vengono restituiti come relazioni ActiveRecord (che consente di utilizzare metodi come last , count , where , pluck , etc)

C’è una gem find_with_order che ti permette di farlo in modo efficiente usando la query SQL nativa.

E supporta sia Mysql che PostgreSQL .

Per esempio:

 Something.find_with_order(array_of_ids) 

Se vuoi una relazione:

 Something.where_with_order(:id, array_of_ids) 

C’è una clausola di ordine in find (: order => ‘…’) che esegue questa operazione quando recupera i record. Puoi ottenere aiuto anche da qui.

link text