Convertendo uma matriz de objetos para ActiveRecord :: Relation

104

Eu tenho uma série de objetos, vamos chamá-la de Indicator. Eu quero executar métodos de classe Indicator (aqueles da def self.subjectsvariedade, escopos, etc) neste array. A única maneira que conheço de executar métodos de classe em um grupo de objetos é fazer com que sejam um ActiveRecord :: Relation. Então acabo recorrendo à adição de um to_indicatorsmétodo a Array.

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

Às vezes, eu encadeamento alguns desses escopos para filtrar os resultados, dentro dos métodos de classe. Portanto, embora eu chame um método em um ActiveRecord :: Relation, não sei como acessar esse objeto. Eu só consigo acessar o conteúdo dela all. Mas allé um Array. Então, eu tenho que converter esse array para um ActiveRecord :: Relation. Por exemplo, isso faz parte de um dos 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

Acho que isso se resume a duas perguntas.

  1. Como posso converter um Array de objetos em um ActiveRecord :: Relation? De preferência sem fazer um a wherecada vez.
  2. Ao executar um def self.subjectsmétodo de tipo em um ActiveRecord :: Relation, como faço para acessar o próprio objeto ActiveRecord :: Relation?

Obrigado. Se eu precisar esclarecer alguma coisa, me avise.

Nathan
fonte
3
Se sua única razão para tentar converter esse array de volta em uma relação é porque você o obteve via .all, apenas use .scopedcomo indica a resposta de Andrew Marshall (embora no rails 4 funcione com .all). Se você precisar transformar um array em uma relação, você errou em algum lugar ...
nzifnab

Respostas:

46

Como posso converter um Array de objetos em um ActiveRecord :: Relation? De preferência, sem fazer um onde cada vez.

Você não pode converter um Array em um ActiveRecord :: Relation, pois um Relation é apenas um construtor para uma consulta SQL e seus métodos não operam em dados reais.

No entanto, se o que você deseja é uma relação:

  • para ActiveRecord 3.x, não chame alle sim chame scoped, o que retornará um Relation que representa os mesmos registros que allvocê daria em um Array.

  • para ActiveRecord 4.x, basta chamar all, que retorna uma relação.

Ao executar um def self.subjectsmétodo de tipo em um ActiveRecord :: Relation, como faço para acessar o próprio objeto ActiveRecord :: Relation?

Quando o método é chamado em um objeto Relation, selfé a relação (em oposição à classe de modelo em que é definida).

Andrew Marshall
fonte
1
Veja @Marco Prins abaixo sobre a solução.
Justin
e o método .push obsoleto no rails 5
Jaswinder
@GstjiSaini Não tenho certeza do método exato ao qual você está se referindo, forneça o documento ou o link da fonte. Porém, se estiver obsoleto, não é uma solução muito viável, pois provavelmente irá embora em breve.
Andrew Marshall
E é por isso que você pode fazer class User; def self.somewhere; where(location: "somewhere"); end; end, entãoUser.limit(5).somewhere
Dorian,
Esta é a única resposta que realmente esclarece o OP. A pergunta revela conhecimento incorreto de como funciona o ActiveRecord. @ Justin está apenas reforçando a falta de compreensão sobre por que é ruim continuar passando arrays de objetos apenas para mapear sobre eles e construir outra consulta desnecessária.
James2m
161

Você pode converter uma matriz de objetos arrem um ActiveRecord :: Relation como este (assumindo que você sabe a qual classe os objetos são, o que provavelmente você sabe)

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

Você tem que usar where, porém, é uma ferramenta útil que você não deve hesitar em usar. E agora você tem uma linha convertendo um array em uma relação.

map(&:id)irá transformar seu array de objetos em um array contendo apenas seus ids. E passar uma matriz para uma cláusula where gerará uma instrução SQL com a INseguinte aparência:

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

Lembre-se de que a ordem do array será perdida - mas como seu objetivo é apenas executar um método de classe na coleção desses objetos, presumo que não seja um problema.

Marco Prins
fonte
3
Muito bem, exatamente o que eu precisava. Você pode usar isso para qualquer atributo no modelo. funciona perfeitamente para mim.
nfriend21
7
Por que construir você mesmo SQL literal quando você pode fazer where(id: arr.map(&:id))? E estritamente falando, isso não converte a matriz em uma relação, mas em vez disso obtém novas instâncias dos objetos (uma vez que a relação é realizada) com aqueles IDs que podem ter valores de atributo diferentes das instâncias já na memória na matriz.
Andrew Marshall
7
Isso perde a ordem, no entanto.
Velizar Hristov,
1
@VelizarHristov Isso porque agora é uma relação, que só pode ser ordenada por uma coluna, e não da maneira que você quiser. As relações são mais rápidas para lidar com grandes conjuntos de dados e haverá algumas compensações.
Marco Prins
8
Muito ineficiente! Você transformou uma coleção de objetos que já tem na memória em objetos que você acessará por meio de uma consulta ao banco de dados. Gostaria de refatorar os métodos de classe que você deseja iterar na matriz.
james2m
5

Bem, no meu caso, eu preciso converter uma matriz de objetos para ActiveRecord :: Relation , bem como classificá- los com uma coluna específica (id por exemplo). Como estou usando o MySQL, a função de campo pode ser útil.

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

O SQL se parece com:

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

Função de campo MySQL

Xingcheng Xia
fonte
Para obter uma versão disso que funcione com PostgreSQL, não procure além deste tópico: stackoverflow.com/questions/1309624/…
armchairdj
0

ActiveRecord::Relation vincula consulta de banco de dados que recupera dados do banco de dados.

Suponha que para fazer sentido, temos array com objetos da mesma classe, então com que consulta supomos ligá-los?

Quando eu 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

Aqui acima, usersretorna o Relationobjeto, mas vincula a consulta do banco de dados por trás dele e você pode visualizá-lo,

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

Portanto, não é possível retornar ActiveRecord::Relationde um array de objetos que seja independente da consulta sql.

raio
fonte
0

Em primeiro lugar, esta NÃO é uma bala de prata. Com base na minha experiência, descobri que a conversão às vezes é mais fácil do que as alternativas. Tento usar essa abordagem com moderação e apenas nos casos em que a alternativa seria mais complexa.

Dito isso, aqui está a minha solução, estendi a Arrayaula

# 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

Um exemplo de uso seria array.to_activerecord_relation.update_all(status: 'finished'). Agora, onde eu uso?

Às vezes, você precisa filtrar, ActiveRecord::Relationpor exemplo, remover elementos não concluídos. Nesses casos, o melhor é usar o escopo elements.not_finishede você ainda manteria ActiveRecord::Relation.

Mas às vezes essa condição é mais complexa. Retire todos os elementos que não estão acabados e que foram produzidos nas últimas 4 semanas e foram inspecionados. Para evitar a criação de novos escopos, você pode filtrar para uma matriz e depois converter de volta. Lembre-se de que você ainda faz uma consulta ao DB, rápido, pois ele pesquisa por, idmas ainda é uma consulta.

Haris Krajina
fonte