ActiveRecord.find (array_of_ids), conservando el orden

98

Cuando lo hace Something.find(array_of_ids)en Rails, el orden de la matriz resultante no depende del orden de array_of_ids.

¿Hay alguna forma de buscar y conservar el orden?

Cajero automático Ordeno manualmente los registros según el orden de las identificaciones, pero eso es un poco escaso.

UPD: si es posible especificar el orden usando el :orderparámetro y algún tipo de cláusula SQL, ¿cómo?

Leonid Shevtsov
fuente

Respuestas:

71

La respuesta es solo para mysql

Hay una función en mysql llamada FIELD ()

Así es como puede usarlo en .find ():

>> ids = [100, 1, 6]
=> [100, 1, 6]

>> WordDocument.find(ids).collect(&:id)
=> [1, 6, 100]

>> WordDocument.find(ids, :order => "field(id, #{ids.join(',')})")
=> [100, 1, 6]

For new Version
>> WordDocument.where(id: ids).order("field(id, #{ids.join ','})")

Actualización: esto se eliminará en el código fuente de Rails 6.1 Rails

kovyrin
fuente
¿Conoce el equivalente de FIELDS()en Postgres?
Trung Lê
3
Escribí una función plpgsql para hacer esto en postgres - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Omar Qureshi
25
Esto ya no funciona. Para Rails más recientes:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff
2
Esta es una mejor solución que la de Gunchars porque no romperá la paginación.
pguardiario
.ids funciona bien para mí, y es bastante rápido documentación de
activerecord
79

Curiosamente, nadie ha sugerido algo como esto:

index = Something.find(array_of_ids).group_by(&:id)
array_of_ids.map { |i| index[i].first }

Tan eficiente como sea, además de permitir que el backend SQL lo haga.

Editar: para mejorar mi propia respuesta, también puede hacerlo así:

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

#index_byy #sliceson adiciones bastante útiles en ActiveSupport para matrices y hashes respectivamente.

Gunchars
fuente
Así que su edición parece funcionar, pero me pone nervioso el orden de las teclas en un hash no está garantizado, ¿verdad? así que cuando llamas a slice y recuperas el hash "reordenado", realmente depende de los valores de retorno de hash en el orden en que se agregaron sus claves. Esto parece depender de un detalle de implementación que puede cambiar.
Jon
2
@Jon, el orden está garantizado en Ruby 1.9 y cualquier otra implementación que intente seguirlo. Para 1.8, Rails (ActiveSupport) parchea la clase Hash para que se comporte de la misma manera, por lo que si está usando Rails, debería estar bien.
Gunchars
gracias por la aclaración, acabo de encontrar eso en la documentación.
Jon
13
El problema con esto es que devuelve una matriz, en lugar de una relación.
Velizar Hristov
3
Genial, sin embargo, el one-liner no funciona para mí (Rails 4.1)
Besi
44

Como dijo Mike Woodhouse en su respuesta , esto ocurre porque, bajo el capó, Rails está usando una consulta SQL con WHERE id IN... clausepara recuperar todos los registros en una consulta. Esto es más rápido que recuperar cada ID individualmente, pero como notó, no conserva el orden de los registros que está recuperando.

Para solucionar este problema, puede ordenar los registros en el nivel de la aplicación de acuerdo con la lista original de ID que utilizó al buscar el registro.

Basado en las muchas respuestas excelentes para ordenar una matriz de acuerdo con los elementos de otra matriz , recomiendo la siguiente solución:

Something.find(array_of_ids).sort_by{|thing| array_of_ids.index thing.id}

O si necesita algo un poco más rápido (pero posiblemente algo menos legible) puede hacer esto:

Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)
Ajedi32
fuente
3
La segunda solución (con index_by) parece fallar para mí, produciendo todos los resultados nulos.
Ben Wheeler
22

Esto parece funcionar para postgresql ( fuente ) y devuelve una relación ActiveRecord

class Something < ActiveRecrd::Base

  scope :for_ids_with_order, ->(ids) {
    order = sanitize_sql_array(
      ["position((',' || id::text || ',') in ?)", ids.join(',') + ',']
    )
    where(:id => ids).order(order)
  }    
end

# usage:
Something.for_ids_with_order([1, 3, 2])

también se puede ampliar para otras columnas, por ejemplo, para la namecolumna, use position(name::text in ?)...

jengibre
fuente
Eres mi héroe de la semana. ¡Gracias!
ntdb
4
Tenga en cuenta que esto solo funciona en casos triviales, eventualmente se encontrará con una situación en la que su Id esté contenido dentro de otros Id en la lista (por ejemplo, encontrará 1 en 11). Una forma de evitar esto es agregar las comas en la verificación de posición, y luego agregar una coma final a la unión, así: order = sanitize_sql_array (["position (',' || clients.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy
Buen punto, @IrishDubGuy! Actualizaré mi respuesta según tu sugerencia. ¡Gracias!
gingerlime
para mí el encadenamiento no funciona. Aquí el nombre de las tablas debe agregarse antes de id: texto como este: ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] versión completa que funcionó para mí: scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } gracias @gingerlime @IrishDubGuy
user1136228
Supongo que debe agregar el nombre de la tabla en caso de que realice algunas uniones ... Eso es bastante común con los ámbitos de ActiveRecord cuando se une.
gingerlime
19

Como respondí aquí , acabo de publicar una gema ( order_as_specified ) que le permite hacer pedidos de SQL nativo como este:

Something.find(array_of_ids).order_as_specified(id: array_of_ids)

Por lo que he podido probar, funciona de forma nativa en todos los RDBMS y devuelve una relación ActiveRecord que se puede encadenar.

JacobEvelyn
fuente
1
Amigo, eres tan increíble. ¡Gracias!
swrobel
5

Desafortunadamente, no es posible en SQL que funcionaría en todos los casos, necesitaría escribir hallazgos únicos para cada registro u orden en ruby, aunque probablemente haya una manera de hacerlo funcionar utilizando técnicas propietarias:

Primer ejemplo:

sorted = arr.inject([]){|res, val| res << Model.find(val)}

MUY INEFICIENTE

Segundo ejemplo:

unsorted = Model.find(arr)
sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}}
Omar Qureshi
fuente
Aunque no es muy eficiente, estoy de acuerdo en que esta solución alternativa es independiente de DB y es aceptable si tiene una pequeña cantidad de filas.
Trung Lê
No use inyectar para esto, es un mapa:sorted = arr.map { |val| Model.find(val) }
tokland
el primero es lento. Estoy de acuerdo con el segundo con un mapa como este:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon
2

La respuesta de @Gunchars es excelente, pero no funciona de manera inmediata en Rails 2.3 porque la clase Hash no está ordenada. Una solución alternativa simple es extender la clase Enumerable ' index_bypara usar la clase OrderedHash:

module Enumerable
  def index_by_with_ordered_hash
    inject(ActiveSupport::OrderedHash.new) do |accum, elem|
      accum[yield(elem)] = elem
      accum
    end
  end
  alias_method_chain :index_by, :ordered_hash
end

Ahora el enfoque de @Gunchars funcionará

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

Prima

module ActiveRecord
  class Base
    def self.find_with_relevance(array_of_ids)
      array_of_ids = Array(array_of_ids) unless array_of_ids.is_a?(Array)
      self.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values
    end
  end
end

Luego

Something.find_with_relevance(array_of_ids)
Chris Bloom
fuente
2

Suponiendo Model.pluck(:id)devoluciones [1,2,3,4]y quieres el orden de[2,4,1,3]

El concepto es utilizar la ORDER BY CASE WHENcláusula SQL. Por ejemplo:

SELECT * FROM colors
  ORDER BY
  CASE
    WHEN code='blue' THEN 1
    WHEN code='yellow' THEN 2
    WHEN code='green' THEN 3
    WHEN code='red' THEN 4
    ELSE 5
  END, name;

En Rails, puede lograr esto al tener un método público en su modelo para construir una estructura similar:

def self.order_by_ids(ids)
  if ids.present?
    order_by = ["CASE"]
    ids.each_with_index do |id, index|
      order_by << "WHEN id='#{id}' THEN #{index}"
    end
    order_by << "END"
    order(order_by.join(" "))
  end
else
  all # If no ids, just return all
end

Entonces hazlo:

ordered_by_ids = [2,4,1,3]

results = Model.where(id: ordered_by_ids).order_by_ids(ordered_by_ids)

results.class # Model::ActiveRecord_Relation < ActiveRecord::Relation

Lo bueno de esto. Los resultados se devuelven como Relaciones ActiveRecord (lo que permite utilizar métodos como last, count, where, pluck, etc)

Christian Fazzini
fuente
2

Hay una gema find_with_order que le permite hacerlo de manera eficiente utilizando una consulta SQL nativa.

Y es compatible con Mysqly PostgreSQL.

Por ejemplo:

Something.find_with_order(array_of_ids)

Si quieres relacion:

Something.where_with_order(:id, array_of_ids)
khiav reoy
fuente
1

Bajo el capó, findcon una matriz de identificadores se generará SELECTuna WHERE id IN...cláusula con a, que debería ser más eficiente que recorrer los identificadores.

Entonces, la solicitud se satisface en un viaje a la base de datos, pero SELECTlos ORDER BYcorreos electrónicos sin cláusulas no están ordenados. ActiveRecord entiende esto, por lo que expandimos nuestro de la findsiguiente manera:

Something.find(array_of_ids, :order => 'id')

Si el orden de los identificadores en su matriz es arbitrario y significativo (es decir, si desea que el orden de las filas se devuelva para que coincida con su matriz, independientemente de la secuencia de identificadores que contiene), entonces creo que sería el mejor servidor procesando posteriormente los resultados en código: podría crear una :ordercláusula, pero sería diabólicamente complicado y no revelaría ninguna intención.

Mike Woodhouse
fuente
Tenga en cuenta que el hash de opciones ha quedado obsoleto. (segundo argumento, en este ejemplo :order => id)
ocodo
1

Aunque no veo que se mencione en ninguna parte de un CHANGELOG, parece que esta funcionalidad se cambió con el lanzamiento de la versión 5.2.0.

Aquí compruebe la actualización de los documentos etiquetados con 5.2.0Sin embargo, parece que también se ha actualizado a la versión 5.0.

Asesino XML
fuente
0

Con referencia a la respuesta aquí

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") funciona para Postgresql.

Sam Kah Chiin
fuente
-4

Hay una cláusula de orden en find (: order => '...') que hace esto al buscar registros. También puede obtener ayuda desde aquí.

Texto del enlace

Ashish Jain
fuente