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?

È stato utile?

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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top