Conversión de una matriz de objetos en ActiveRecord :: Relation

104

Tengo una matriz de objetos, llamémoslo un Indicator. Quiero ejecutar métodos de clase de indicador (los de la def self.subjectsvariedad, alcances, etc.) en esta matriz. La única forma que conozco de ejecutar métodos de clase en un grupo de objetos es que sean un ActiveRecord :: Relation. Entonces termino recurriendo a agregar un to_indicatorsmétodo a Array.

def to_indicators
  # TODO: Make this less terrible.
  Indicator.where id: self.pluck(:id)
end

A veces encadeno bastantes de estos ámbitos para filtrar los resultados, dentro de los métodos de clase. Entonces, aunque llamo a un método en un ActiveRecord :: Relation, no sé cómo acceder a ese objeto. Solo puedo acceder a su contenido all. Pero alles una matriz. Entonces tengo que convertir esa matriz en un ActiveRecord :: Relation. Por ejemplo, esto es parte de uno de los métodos:

all.to_indicators.applicable_for_bank(id).each do |indicator|
  total += indicator.residual_risk_for(id)
  indicator_count += 1 if indicator.completed_by?(id)
end

Supongo que esto se condensa en dos preguntas.

  1. ¿Cómo puedo convertir una matriz de objetos en un ActiveRecord :: Relation? Preferiblemente sin hacer una wherecada vez.
  2. Al ejecutar un def self.subjectsmétodo de tipo en un ActiveRecord :: Relation, ¿cómo accedo a ese objeto ActiveRecord :: Relation?

Gracias. Si necesito aclarar algo, házmelo saber.

Nathan
fuente
3
Si su única razón para intentar convertir esa matriz de nuevo en una relación es porque la obtuvo a través de .all, simplemente use .scopedcomo indica la respuesta de Andrew Marshall (aunque en los rieles 4 funcionará .all). Si necesita convertir una matriz en una relación, se ha equivocado en algún lugar ...
nzifnab

Respuestas:

46

¿Cómo puedo convertir una matriz de objetos en un ActiveRecord :: Relation? Preferiblemente sin hacer un donde cada vez.

No puede convertir un Array en un ActiveRecord :: Relation ya que un Relation es solo un constructor para una consulta SQL y sus métodos no operan con datos reales.

Sin embargo, si lo que quieres es una relación, entonces:

  • para ActiveRecord 3.x, no llame ally, en su lugar scoped, llame , lo que devolverá una Relación que representa los mismos registros que allle daría en un Array.

  • para ActiveRecord 4.x, simplemente llame all, que devuelve una Relación.

Al ejecutar un def self.subjectsmétodo de tipo en un ActiveRecord :: Relation, ¿cómo accedo a ese objeto ActiveRecord :: Relation?

Cuando se llama al método en un objeto Relation, selfes la relación (a diferencia de la clase de modelo en la que se define).

Andrew Marshall
fuente
1
Vea @Marco Prins a continuación sobre la solución.
Justin
¿Qué pasa con el método obsoleto .push en rieles 5
Jaswinder
@GstjiSaini No estoy seguro del método exacto al que se refiere, proporcione el documento o el enlace de la fuente. Sin embargo, si está obsoleto, no es una solución muy viable, ya que probablemente desaparecerá pronto.
Andrew Marshall
Y es por eso que puedes hacerlo class User; def self.somewhere; where(location: "somewhere"); end; end, entoncesUser.limit(5).somewhere
Dorian
Esta es la única respuesta que realmente ilumina al OP. La pregunta revela un conocimiento defectuoso de cómo funciona ActiveRecord. @Justin solo está reforzando la falta de comprensión sobre por qué es malo seguir pasando matrices de objetos solo para mapearlos y construir otra consulta innecesaria.
james2m
161

Puede convertir una matriz de objetos arren un ActiveRecord :: Relation como este (suponiendo que sepa qué clase son los objetos, lo que probablemente sepa)

MyModel.where(id: arr.map(&:id))

Sin whereembargo, debe usarla , es una herramienta útil que no debería ser reacio a usar. Y ahora tiene una línea que convierte una matriz en una relación.

map(&:id)convertirá su matriz de objetos en una matriz que contiene solo sus identificaciones. Y pasar una matriz a una cláusula where generará una declaración SQL con un INaspecto similar a:

SELECT .... WHERE `my_models`.id IN (2, 3, 4, 6, ....

Tenga en cuenta que el orden de la matriz se perderá , pero dado que su objetivo es solo ejecutar un método de clase en la colección de estos objetos, supongo que no será un problema.

Marco Prins
fuente
3
Bien hecho, exactamente lo que necesitaba. Puede usar esto para cualquier atributo del modelo. funciona perfectamente para mí.
nfriend21
7
¿Por qué construir SQL literal usted mismo cuando puede hacerlo where(id: arr.map(&:id))? Y estrictamente hablando, esto no convierte la matriz en una relación, sino que obtiene nuevas instancias de los objetos (una vez que se realiza la relación) con esos ID que pueden tener valores de atributo diferentes a las instancias que ya están en memoria en la matriz.
Andrew Marshall
7
Sin embargo, esto pierde el orden.
Velizar Hristov
1
@VelizarHristov Eso es porque ahora es una relación, que solo se puede ordenar por una columna, y no de la forma que desee. Las relaciones son más rápidas para manejar grandes conjuntos de datos y habrá algunas compensaciones.
Marco Prins
8
¡Muy ineficiente! Ha convertido una colección de objetos que ya tiene en la memoria en objetos para los que va a realizar una consulta de base de datos para acceder. Me gustaría refactorizar esos métodos de clase que desea iterar sobre la matriz.
james2m
5

Bueno, en mi caso, necesito convertir una matriz de objetos a ActiveRecord :: Relation , así como ordenar con una columna específica (Identificación por ejemplo). Como estoy usando MySQL, la función de campo podría ser útil.

MyModel.where('id in (?)',ids).order("field(id,#{ids.join(",")})") 

El SQL se parece a:

SELECT ... FROM ... WHERE (id in (11,5,6,7,8,9,10))  
ORDER BY field(id,11,5,6,7,8,9,10)

Función de campo MySQL

Xingcheng Xia
fuente
Para obtener una versión de esto que funcione con PostgreSQL, no busque más allá de este hilo: stackoverflow.com/questions/1309624/…
armchairdj
0

ActiveRecord::Relation enlaza la consulta de la base de datos que recupera datos de la base de datos.

Supongamos que tiene sentido, tenemos una matriz con objetos de la misma clase, entonces ¿con qué consulta se supone que los vinculamos?

Cuando corro

users = User.where(id: [1,3,4,5])
  User Load (0.6ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 3, 4, 5)  ORDER BY created_at desc

Aquí arriba, usersdevuelve el Relationobjeto pero enlaza la consulta de la base de datos detrás de él y puede verlo,

users.to_sql
 => "SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 3, 4, 5)  ORDER BY created_at desc"

Por lo tanto, no es posible regresar ActiveRecord::Relationde una matriz de objetos que es independiente de la consulta SQL.

rayo
fuente
0

En primer lugar, esta NO es una fórmula mágica. Según mi experiencia, descubrí que convertir a una relación a veces es más fácil que las alternativas. Intento utilizar este enfoque con moderación y solo en los casos en que la alternativa sería más compleja.

Dicho esto, aquí está mi solución, he ampliado la Arrayclase

# lib/core_ext/array.rb

class Array

  def to_activerecord_relation
    return ApplicationRecord.none if self.empty?

    clazzes = self.collect(&:class).uniq
    raise 'Array cannot be converted to ActiveRecord::Relation since it does not have same elements' if clazzes.size > 1

    clazz = clazzes.first
    raise 'Element class is not ApplicationRecord and as such cannot be converted' unless clazz.ancestors.include? ApplicationRecord

    clazz.where(id: self.collect(&:id))
  end
end

Un ejemplo de uso sería array.to_activerecord_relation.update_all(status: 'finished') . ¿Ahora dónde lo uso?

A veces es necesario filtrar, ActiveRecord::Relationpor ejemplo, eliminar elementos no completados. En esos casos, lo mejor es usar el alcanceelements.not_finished y aún lo mantendría ActiveRecord::Relation.

Pero a veces esa condición es más compleja. Sacar todos los elementos que no estén terminados, que hayan sido producidos en las últimas 4 semanas y hayan sido inspeccionados. Para evitar la creación de nuevos ámbitos, puede filtrar a una matriz y luego volver a convertir. Tenga en cuenta que aún realiza una consulta a DB, rápido ya que busca por idpero sigue siendo una consulta.

Haris Krajina
fuente