ruby-on-rails - not - rails find_by_id
Manera limpia de encontrar objetos ActiveRecord por id en el orden especificado (10)
Aparentemente mySQL y otro sistema de administración de BD ordenan las cosas por sí mismos. Creo que puedes eludir ese hacer:
ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join('','')})" )
Quiero obtener una matriz de objetos ActiveRecord dado un conjunto de ids.
lo asumo
Object.find([5,2,3])
Devolvería una matriz con el objeto 5, el objeto 2, luego el objeto 3 en ese orden, pero en su lugar obtengo una matriz ordenada como objeto 2, objeto 3 y luego objeto 5.
La API de método de búsqueda de Base de ActiveRecord menciona que no debe esperar en el orden proporcionado (otra documentación no proporciona esta advertencia).
¿Una solución potencial fue dada en Buscar por un conjunto de identificadores en el mismo orden? , pero la opción de orden no parece ser válida para SQLite.
Puedo escribir algún código de rubí para ordenar los objetos por mí mismo (ya sea de forma simple y escalando a menor escala o escalando mejor y más complejo), pero ¿hay una forma mejor?
Aquí está lo más simple que se me ocurre:
ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
Aquí hay un rendimiento (hash-lookup, no O (n) array search como en detect!) One-liner, como método:
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
Basado en mi comentario anterior a Jeroen van Dijk puedes hacer esto de manera más eficiente y en dos líneas 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]}
Como referencia aquí está el punto de referencia que utilicé
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
Ahora el otro
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
Actualizar
Puede hacer esto en la mayoría de las declaraciones de pedido y caso, aquí hay un método de clase que podría usar.
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]
Como respondí here , acabo de lanzar una gema ( order_as_specified ) que le permite hacer un ordenamiento SQL nativo como este:
Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])
Acabo de probar y funciona en SQLite.
Justin Weiss escribió un artículo en el blog sobre este problema hace solo dos días.
Parece ser un buen método para informar a la base de datos sobre el orden preferido y cargar todos los registros ordenados en ese orden directamente desde la base de datos. Ejemplo de su artículo de 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)
Eso te permite escribir:
Object.find_ordered([2, 1, 3]) # => [2, 1, 3]
No es que MySQL y otras DBs arreglen las cosas por sí mismas, es que no las clasifican. Cuando llama a Model.find([5, 2, 3])
, el SQL generado es algo así como:
SELECT * FROM models WHERE models.id IN (5, 2, 3)
Esto no especifica un pedido, solo el conjunto de registros que desea devolver. Resulta que generalmente MySQL devolverá las filas de la base de datos en ''id''
orden ''id''
, pero no hay garantía de esto.
La única forma de hacer que la base de datos devuelva registros en un orden garantizado es agregar una cláusula de orden. Si sus registros siempre serán devueltos en un orden particular, entonces puede agregar una columna de ordenación al Model.find([5, 2, 3], :order => ''sort_column'')
db y hacer Model.find([5, 2, 3], :order => ''sort_column'')
. Si este no es el caso, tendrá que hacer la clasificación en el código:
ids = [5, 2, 3]
records = Model.find(ids)
sorted_records = ids.collect {|id| records.detect {|x| x.id == id}}
Otra forma (probablemente más eficiente) de hacerlo en 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] }
Una solución portátil sería usar una declaración SQL CASE en su ORDER BY. Puede usar casi cualquier expresión en un ORDER BY y un CASE se puede usar como una tabla de búsqueda integrada. Por ejemplo, el SQL que está buscando se vería así:
select ...
order by
case id
when 5 then 0
when 2 then 1
when 3 then 2
end
Es bastante fácil de generar con un poco de Ruby:
ids = [5, 2, 3]
order = ''case id '' + (0 .. ids.length).map { |i| "when #{ids[i]} then #{i}" }.join('' '') + '' end''
Lo anterior supone que estás trabajando con números u otros valores seguros en los ids
; si ese no es el caso, entonces querrá usar connection.quote
o uno de los métodos de sanitización ActiveRecord SQL para citar correctamente sus ids
.
Luego use la secuencia de order
como su condición de pedido:
Object.find(ids, :order => order)
o en el mundo moderno:
Object.where(:id => ids).order(order)
Esto es un poco detallado, pero debería funcionar igual con cualquier base de datos SQL y no es tan difícil ocultar la fealdad.
@things = [5,2,3].map{|id| Object.find(id)}
Esta es probablemente la forma más fácil, suponiendo que no tiene demasiados objetos para encontrar, ya que requiere un viaje a la base de datos para cada identificación.