ruby on rails - rails - ActiveRecord.find(array_of_ids), preservando el orden
rails sql query (10)
Cuando realiza Something.find(array_of_ids)
en Rails, el orden de la matriz resultante no depende del orden de array_of_ids
.
¿Hay alguna manera de hacer el hallazgo y preservar el orden?
ATM: ordeno manualmente los registros en función del orden de los ID, pero eso es una especie de cojera.
UPD: si es posible especificar el orden usando :order
param y algún tipo de cláusula SQL, ¿cómo?
Aunque no veo que se mencione en ningún lugar en un CHANGELOG, parece que esta funcionalidad se modificó con el lanzamiento de la versión 5.2.0
.
Aquí se commit actualizar los documentos etiquetados con 5.2.0
Sin embargo, parece que también se ha backported a la versión 5.0
.
Como declaró en su respuesta , esto ocurre porque, bajo el capó, Rails está utilizando una consulta SQL con una WHERE id IN... clause
para recuperar todos los registros en una consulta. Esto es más rápido que recuperar cada identificación individualmente, pero como notó, no conserva el orden de los registros que está recuperando.
Para solucionar esto, puede ordenar los registros en el nivel de aplicación de acuerdo con la lista original de ID que utilizó al buscar el registro.
Basándome en las muchas respuestas excelentes para Ordenar una matriz según 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)
Como respondí here , acabo de lanzar una gema ( order_as_specified ) que le permite hacer un ordenamiento 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 de ActiveRecord que se puede encadenar.
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 se obtiene además de permitir que SQL back-end 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_by
y #slice
son #slice
bastante útiles en ActiveSupport para matrices y hash, respectivamente.
Debajo del capó, find
con una serie de identificadores generará un SELECT
con una cláusula WHERE id IN...
que debería ser más eficiente que recorrer los identificadores.
De modo que la solicitud se satisface en un viaje a la base de datos, pero los SELECT
s sin las cláusulas ORDER BY
están desordenadas. ActiveRecord entiende esto, por lo que ampliamos nuestro find
siguiente manera:
Something.find(array_of_ids, :order => ''id'')
Si el orden de los identificadores en su matriz es arbitrario y significativo (es decir, desea que el orden de las filas devueltas coincida con su matriz, independientemente de la secuencia de identificadores que contenga), creo que sería mejor servidor procesando los resultados en código: podrías construir una cláusula :order
pero sería diabólicamente complicado y para nada revelador de intenciones.
Esto parece funcionar para postgresql ( source ) - 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])
puede extenderse también para otras columnas, por ejemplo, para la columna de name
, usar position(name::text in ?)
...
Hay una cláusula de orden en find (: order => ''...'') que hace esto al recuperar registros. Puedes obtener ayuda de aquí también.
La respuesta de @Gunchars es genial, pero no funciona de fábrica en Rails 2.3 porque la clase Hash no está ordenada. Una solución simple es extender la clase Enumerable '' index_by
para 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
Entonces
Something.find_with_relevance(array_of_ids)
No es posible en SQL que funcione en todos los casos, desafortunadamente, necesitaría escribir hallazgos individuales para cada registro u orden en ruby, aunque probablemente haya una manera de hacerlo funcionar usando técnicas patentadas:
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}}
La respuesta es solo para mysql
Hay una función en mysql llamada FIELD()
Aquí es cómo puedes 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]