Modo pulito per trovare oggetti ActiveRecord per id nell’ordine specificato

Voglio ottenere una matrice di oggetti ActiveRecord data una serie di ID.

L’ho assunto

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

Restituirebbe un array con l’object 5, l’object 2, quindi l’object 3 in quell’ordine, ma invece ottengo un array ordinato come object 2, object 3 e quindi object 5.

L’ API metodo metodo ActiveRecord Base menziona che non dovresti aspettarti nell’ordine fornito (altra documentazione non fornisce questo avviso).

Una ansible soluzione è stata data in Trova per serie di ID nello stesso ordine? , ma l’opzione ordine non sembra essere valida per SQLite.

Posso scrivere qualche codice ruby per ordinare gli oggetti da solo (sia un po ‘semplice e scarsamente ridimensionante o migliore scalabilità e più complesso), ma c’è un modo migliore?

Non è che MySQL e altri DB ordinino le cose da soli, è che non li ordinano. Quando chiamate Model.find([5, 2, 3]) , l’SQL generato è qualcosa del tipo:

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

Questo non specifica un ordine, solo l’insieme di record che desideri vengano restituiti. Risulta che generalmente MySQL restituirà le righe del database in ordine 'id' , ma non è garantito.

L’unico modo per ottenere il database per restituire i record in un ordine garantito è aggiungere una clausola di ordine. Se i record vengono sempre restituiti in un ordine particolare, è ansible aggiungere una colonna di ordinamento al db e fare Model.find([5, 2, 3], :order => 'sort_column') . Se questo non è il caso, dovrai eseguire l’ordinamento nel codice:

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

Sulla base del mio precedente commento a Jeroen van Dijk puoi farlo in modo più efficiente e in due righe usando 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]} 

Per riferimento qui è il punto di riferimento che ho usato

 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 

Ora l’altro

 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 

Aggiornare

Puoi farlo nella maggior parte dei casi utilizzando le dichiarazioni di ordini e casi, ecco un metodo di class che potresti usare.

 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] 

Apparentemente mySQL e altri sistemi di gestione DB ordinano le cose da sole. Penso che puoi ignorare questo:

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

Una soluzione portatile sarebbe quella di utilizzare un’istruzione SQL CASE nel tuo ORDER BY. Puoi usare praticamente qualsiasi espressione in un ORDER BY e un CASE può essere usato come una tabella di ricerca in linea. Ad esempio, l’SQL che stai cercando sarebbe simile a questo:

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

È abbastanza facile da generare con un po ‘di Ruby:

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

Quanto sopra presuppone che tu stia lavorando con numeri o altri valori sicuri negli ids ; in caso contrario, si consiglia di utilizzare connection.quote o uno dei metodi sanitizer SQL ActiveRecord per quotare correttamente i propri ids .

Quindi utilizza la stringa order come condizione per l’ordine:

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

o nel mondo moderno:

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

Questo è un po ‘prolisso ma dovrebbe funzionare allo stesso modo con qualsiasi database SQL e non è così difficile hide la bruttezza.

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

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

Appena testato e funziona in SQLite.

Justin Weiss ha scritto un articolo del blog su questo problema solo due giorni fa.

Sembra essere un buon approccio per comunicare al database l’ordine preferito e caricare tutti i record ordinati in quell’ordine direttamente dal database. Esempio dal suo articolo del 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) 

Questo ti permette di scrivere:

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

Un altro (probabilmente più efficiente) modo di farlo in 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] } 

Ecco la cosa più semplice che potrei inventare:

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

Ecco un performante (ricerca di hash, non ricerca di array O (n) come in detect!) One-liner, come metodo:

 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 
 @things = [5,2,3].map{|id| Object.find(id)} 

Questo è probabilmente il modo più semplice, supponendo che non ci siano troppi oggetti da trovare, poiché richiede un viaggio nel database per ciascun id.