Pregunta

Quiero obtener una matriz de objetos ActiveRecord dada una matriz de identificadores.

Supuse que

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

Devuelve 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 base ActiveRecord API de método de búsqueda menciona que no debería esperarlo en el orden proporcionado (otra documentación no da esta advertencia).

Se proporcionó una solución potencial en Buscar por conjunto de identificadores en el mismo pedido? , pero la opción de pedido no parece ser válida para SQLite.

Puedo escribir un código ruby ??para ordenar los objetos yo mismo (ya sea algo simple y escalando mal o mejor escalando y más complejo), pero ¿hay una mejor manera?

¿Fue útil?

Solución

No es que MySQL y otros DB clasifiquen las cosas por sí mismos, es que no los 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 orden, solo el conjunto de registros que desea devolver. Resulta que generalmente MySQL devolverá las filas de la base de datos en el orden 'id' , pero no hay garantía de esto.

La única forma de hacer que la base de datos devuelva registros en un pedido garantizado es agregar una cláusula de pedido. Si sus registros siempre serán devueltos en un orden particular, entonces puede agregar una columna de ordenación a la base de datos y hacer Model.find ([5, 2, 3],: order = > 'sort_column') . Si este no es el caso, tendrá que ordenar el código:

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

Otros consejos

Basado en mi comentario anterior a Jeroen van Dijk, puede hacerlo 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]}

Para referencia aquí es 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 los casos utilizando declaraciones de orden 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]

Aparentemente, mySQL y otros sistemas de administración de bases de datos clasifican las cosas por su cuenta. Creo que puedes evitar eso haciendo:

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

Una solución portátil sería utilizar una instrucción SQL CASE en su ORDER BY. Puede usar casi cualquier expresión en un ORDER BY y un CASE puede usarse como una tabla de búsqueda en línea. Por ejemplo, el SQL que busca se vería así:

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

Eso 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á trabajando con números u otros valores seguros en ids ; si ese no es el caso, entonces querrá usar < code> connection.quote o uno de los métodos de desinfectante ActiveRecord SQL para citar correctamente sus ids .

Luego use la cadena 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.

Como respondí aquí , acabo de lanzar una gema ( order_as_specified ) que le permite realizar pedidos SQL nativos 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 de blog sobre este problema hace solo dos días.

Parece ser un buen enfoque 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]

Aquí hay un performant (búsqueda de hash, no O (n) búsqueda de matriz 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

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] }

Esto es lo más simple que se me ocurrió:

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)}

Esta es probablemente la forma más fácil, suponiendo que no tenga demasiados objetos para encontrar, ya que requiere un viaje a la base de datos para cada ID.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top