Modo pulito per trovare oggetti ActiveRecord per ID nell'ordine specificato
-
03-07-2019 - |
Domanda
Voglio ottenere un array di oggetti ActiveRecord dato un array di ID.
L'ho assunto
Object.find([5,2,3])
Restituirebbe un array con oggetto 5, oggetto 2, quindi oggetto 3 in quell'ordine, ma invece ottengo un array ordinato come oggetto 2, oggetto 3 e quindi oggetto 5.
La base ActiveRecord trova l'API del metodo menziona che non dovresti aspettalo nell'ordine fornito (altra documentazione non fornisce questo avviso).
Una potenziale soluzione è stata fornita in Trova per array di ID nella stessa ordine? , ma l'opzione ordine non sembra essere valida per SQLite.
Riesco a scrivere un po 'di codice ruby ??per ordinare gli oggetti da solo (o in qualche modo un ridimensionamento poco semplice o un ridimensionamento migliore e più complesso), ma esiste un modo migliore?
Soluzione
Non è che MySQL e altri DB ordinino le cose da soli, è che non le ordinano. Quando chiami Model.find ([5, 2, 3])
, l'SQL generato è simile a:
SELECT * FROM models WHERE models.id IN (5, 2, 3)
Questo non specifica un ordine, ma solo il set di record che vuoi restituire. Si scopre che generalmente MySQL restituirà le righe del database nell'ordine 'id'
, ma non esiste alcuna garanzia.
L'unico modo per ottenere il database per restituire i record in un ordine garantito è aggiungere una clausola d'ordine. Se i tuoi record verranno sempre restituiti in un ordine particolare, puoi aggiungere una colonna di ordinamento al db ed eseguire Model.find ([5, 2, 3],: order = > 'sort_column')
. In caso contrario, dovrai eseguire l'ordinamento in codice:
ids = [5, 2, 3]
records = Model.find(ids)
sorted_records = ids.collect {|id| records.detect {|x| x.id == id}}
Altri suggerimenti
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 benchmark 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
Aggiorna
Puoi farlo nella maggior parte usando le istruzioni order e case, ecco un metodo di classe 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 soli. Penso che tu possa bypassare ciò facendo:
ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
Una soluzione portatile sarebbe quella di utilizzare un'istruzione CASE SQL in ORDER BY. È possibile utilizzare praticamente qualsiasi espressione in un ORDER BY e un CASE può essere utilizzato come tabella di ricerca incorporata. Ad esempio, l'SQL che stai cercando sarebbe simile al seguente:
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 in ids
; in caso contrario, ti consigliamo di utilizzare < code> connection.quote o uno dei metodi di disinfezione SQL ActiveRecord per citare correttamente i tuoi ID
.
Quindi usa la stringa order
come condizione di ordinazione:
Object.find(ids, :order => order)
o nel mondo moderno:
Object.where(:id => ids).order(order)
Questo è un po 'dettagliato ma dovrebbe funzionare allo stesso modo con qualsiasi database SQL e non è così difficile nascondere la bruttezza.
Mentre ho risposto qui , ho appena rilasciato una gemma ( order_as_specified ) che ti consente di eseguire l'ordinamento SQL nativo in questo modo:
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 di 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)
Ciò ti consente di scrivere:
Object.find_ordered([2, 1, 3]) # => [2, 1, 3]
Ecco un performer (hash-lookup, non O (n) array 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
Un altro modo (probabilmente più efficiente) per 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 }
@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 ogni ID.