rails not find_by_id find_by all ruby-on-rails sqlite activerecord find rails-activerecord

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.