Existe uma maneira de obter uma coleção de todos os modelos no seu aplicativo Rails?

201

Existe uma maneira de obter uma coleção de todos os modelos no seu aplicativo Rails?

Basicamente, posso fazer o seguinte: -

Models.each do |model|
  puts model.class.name
end
mr_urf
fonte
1
Se você precisa coletar todos os modelos, incluindo modelos de Rails motores / railties, ver a resposta por @jaime
Andrei
Não funciona em trilhos 5.1
aks

Respostas:

98

Edição: Veja os comentários e outras respostas. Existem respostas mais inteligentes do que esta! Ou tente melhorar este como wiki da comunidade.

Os modelos não se registram em um objeto mestre; portanto, o Rails não possui a lista de modelos.

Mas você ainda pode procurar no conteúdo do diretório de modelos do seu aplicativo ...

Dir.foreach("#{RAILS_ROOT}/app/models") do |model_path|
  # ...
end

Edição: Outra idéia (selvagem) seria usar a reflexão Ruby para procurar todas as classes que estendem ActiveRecord :: Base. Não sei como é possível listar todas as classes ...

EDIT: Apenas por diversão, encontrei uma maneira de listar todas as aulas

Module.constants.select { |c| (eval c).is_a? Class }

EDIT: finalmente conseguiu listar todos os modelos sem consultar os diretórios

Module.constants.select do |constant_name|
  constant = eval constant_name
  if not constant.nil? and constant.is_a? Class and constant.superclass == ActiveRecord::Base
    constant
  end
end

Se você quiser lidar com a classe derivada também, precisará testar toda a cadeia da superclasse. Eu fiz isso adicionando um método à classe Class:

class Class
  def extend?(klass)
    not superclass.nil? and ( superclass == klass or superclass.extend? klass )
  end
end

def models 
  Module.constants.select do |constant_name|
    constant = eval constant_name
    if not constant.nil? and constant.is_a? Class and constant.extend? ActiveRecord::Base
    constant
    end
  end
end
Vincent Robert
fonte
6
Para sua informação, cronometrei os dois métodos apenas por diversão. Procurar nos diretórios é uma ordem de magnitude mais rápida do que pesquisar nas classes. Isso era provavelmente óbvio, mas agora você sabe :)
Edward Anderson
9
Além disso, é importante observar que a pesquisa de modelos por meio dos métodos constantes não incluirá nada que não tenha sido referenciado desde o início do aplicativo, pois ele carrega apenas os modelos sob demanda.
Edward Anderson
4
Eu prefiro 'Kernel.const_get constant_name' a 'eval constant_name'.
precisa
3
RAILS_ROOTnão está mais disponível no Rails 3. Em vez disso, useDir.glob(Rails.root.join('app/models/*'))
fanaugen
1
Na verdade, os modelos se registram como descendentes de ActiveRecord::Baseagora, portanto, se você carregar todos os modelos com entusiasmo, poderá iterá-los facilmente - veja minha resposta abaixo.
sj26
393

A resposta completa para o Rails 3, 4 e 5 é:

Se cache_classesestiver desativado (por padrão, está desativado no desenvolvimento, mas ativado na produção):

Rails.application.eager_load!

Então:

ActiveRecord::Base.descendants

Isso garante que todos os modelos em seu aplicativo, independentemente de onde eles estejam, estejam carregados e quaisquer gemas que você esteja usando que forneçam modelos também sejam carregadas.

Isso também deve funcionar em classes que herdam de ActiveRecord::Base , como ApplicationRecordno Rails 5, e retornam apenas a subárvore de descendentes:

ApplicationRecord.descendants

Se você quiser saber mais sobre como isso é feito, consulte ActiveSupport :: DescendantsTracker .

sj26
fonte
33
Impressionante! Essa deve ser a resposta aceita. Para quem estiver usando isso em uma tarefa rake: faça com que sua tarefa dependa :environmentpara que o eager_load!trabalho funcione.
precisa saber é
1
Ou, como a ligeiramente alternativa mais rápida Rails.application.eager_load!, você pode apenas carregar os modelos:Dir.glob(Rails.root.join('app/models/*')).each do |x| require x end
Ajedi32
5
@ Ajedi32 que não está completo, os modelos podem ser definidos fora desses diretórios, especialmente ao usar motores com modelos. Um pouco melhor, pelo menos glob todos os Rails.paths["app/models"].existentdiretórios. O carregamento ansioso de todo o aplicativo é uma resposta mais completa e garantirá que não haja absolutamente nenhum lugar para os modelos serem definidos.
sj26
2
Entendi o que sj26 significa, mas talvez haja um pequeno erro: até onde sei no ambiente de desenvolvimento, cache_classes está desativado (false), é por isso que você precisa carregar manualmente o aplicativo manualmente para acessar todos os modelos. explicado aqui
masciugo 17/10
3
@ Ajedi32 novamente, não a resposta completa. Se você deseja carregar apenas modelos com entusiasmo, tente:Rails.application.paths["app/models"].eager_load!
sj26
119

Apenas para o caso de alguém tropeçar nessa, eu tenho outra solução, não confiando na leitura de dir ou estendendo a classe Class ...

ActiveRecord::Base.send :subclasses

Isso retornará uma matriz de classes. Então você pode fazer

ActiveRecord::Base.send(:subclasses).map(&:name)
kikito
fonte
8
por que você não usa, ActiveRecord::Base.subclassesmas tem que usar send? Além disso, parece que você precisa "tocar" o modelo antes que ele apareça, por exemplo, c = Category.newe ele apareça. Caso contrário, não vai.
nonopolarity
52
Em trilhos 3, esta foi alterada paraActiveRecord::Base.descendants
Tobias Cohen
3
Você precisa usar "send" porque o membro: subclasses está protegido.
Kevin Rood
11
Obrigado pela dica do Rails 3. Para qualquer um que ActiveRecord::Base.descendantsaparecer , você ainda precisará "tocar" os modelos antes para listá-los.
Nfm 03/07/19
3
Tecnicamente no Rails 3 você tem subclasses e descendentes, eles significam coisas diferentes.
Sj26
67
ActiveRecord::Base.connection.tables.map do |model|
  model.capitalize.singularize.camelize
end

retornará

["Article", "MenuItem", "Post", "ZebraStripePerson"]

Informações adicionais Se você desejar chamar um método no nome do objeto sem modelo: método desconhecido da cadeia ou erros de variáveis, use este

model.classify.constantize.attribute_names
lightyrs
fonte
8
Isso fornecerá todas as tabelas, não apenas os modelos, pois algumas tabelas nem sempre têm modelos associados.
courtsimas
Essa resposta deve ser considerada incorreta, pois é possível (e comum em configurações herdadas) configurar o nome da tabela como algo diferente do nome pluralizado do modelo. Esta resposta fornece a resposta correta, mesmo quando a instalação se desvia da configuração padrão.
Lorefnon # 22/16
em alguns casos, isso funciona melhor do que ActiveRecord::Base.send :subclassesprocurar os nomes das tabelas é uma boa idéia. A geração automática dos nomes dos modelos pode ser problemática, como mencionado anteriormente.
Tilo
.capitalize.singularize.camelizepode ser substituído por .classify.
Maxim
34

Procurei maneiras de fazer isso e acabei escolhendo desta maneira:

in the controller:
    @data_tables = ActiveRecord::Base.connection.tables

in the view:
  <% @data_tables.each do |dt|  %>
  <br>
  <%= dt %>
  <% end %>
  <br>

fonte: http://portfo.li/rails/348561-how-can-one-list-all-database-tables-from-one-project

jaime
fonte
1
Esta é a única maneira de obter TODOS os modelos, incluindo modelos de mecanismos Rails usados ​​no aplicativo. Obrigado pela dica!
Andrei
2
Alguns métodos úteis: ActiveRecord::Base.connection.tables.each{|t| begin puts "%s: %d" % [t.humanize, t.classify.constantize.count] rescue nil end}Alguns dos modelos podem não estar ativados; portanto, é necessário resgatá-lo.
Andrei
2
Adaptação @ Andrei é um pouco: model_classes = ActiveRecord::Base.connection.tables.collect{|t| t.classify.constantize rescue nil }.compact
Max Williams
30

Para os modelos Rails5 , agora são subclasses de ApplicationRecord, para obter a lista de todos os modelos em seu aplicativo, você faz:

ApplicationRecord.descendants.collect { |type| type.name }

Ou mais curto:

ApplicationRecord.descendants.collect(&:name)

Se você estiver no modo dev, precisará de modelos de carregamento ansiosos antes:

Rails.application.eager_load!
Nimir
fonte
1
Entendo que isso exigiria que as classes já estivessem carregadas e daria resultados incompletos no ambiente de desenvolvimento com o carregamento automático ativado. Não vou votar, mas talvez isso deva ser mencionado na resposta.
Lorefnon # 22/16
tarifa suficiente, atualizando
Nimir 22/05
Estou no Rails 6.0.2 e o eager_load! não criou o método descendentes para retornar nada além de uma matriz vazia.
jgomo3 17/02
23

Acho que a solução da @ hnovick é legal se você não tem modelos sem mesa. Essa solução funcionaria também no modo de desenvolvimento

Minha abordagem é sutilmente diferente -

ActiveRecord::Base.connection.tables.map{|x|x.classify.safe_constantize}.compact

classify deve fornecer o nome da classe de uma string corretamente . safe_constantize garante que você possa transformá-lo em uma classe com segurança, sem gerar uma exceção. Isso é necessário caso você tenha tabelas de banco de dados que não são modelos. compacto para que quaisquer nulos na enumeração sejam removidos.

Aditya Sanghi
fonte
3
Isso é incrível @Aditya Sanghi. Eu não sabia safe_constantize.
lightyrs 18/09/12
Para trilhos 2.3.x, use: ActiveRecord :: Base.connection.tables.map {| x | x.classify.constantize resgate nil} .compact
iheggie
@ iheggie Geralmente, é melhor postar isso como uma resposta separada do que editá-lo na postagem existente.
Pokechu22
obrigado, eu encontrei você responder o mais adequado para mim #adiya
ilusionista
21

Se você deseja apenas os nomes das classes:

ActiveRecord::Base.descendants.map {|f| puts f}

Basta executá-lo no console do Rails, nada mais. Boa sorte!

EDIT: @ sj26 está certo, você precisa executá-lo primeiro antes de poder chamar os descendentes:

Rails.application.eager_load!
Jordan Michael Rushing
fonte
Apenas o que eu queria. Obrigado!
Sunsations
ligando mapcom puts? Eu não entendo o ponto deve serActiveRecord::Base.descendants.map(&:model_name)
Nuno Costa
Você pode fazer dessa maneira, mas eles estarão em uma única matriz, em vez de linha por linha, em um formato muito mais fácil de ler.
Jordan Michael Rushing
17

Isso parece funcionar para mim:

  Dir.glob(RAILS_ROOT + '/app/models/*.rb').each { |file| require file }
  @models = Object.subclasses_of(ActiveRecord::Base)

O Rails só carrega modelos quando eles são usados, portanto a linha Dir.glob "requer" todos os arquivos no diretório models.

Depois de ter os modelos em uma matriz, você pode fazer o que estava pensando (por exemplo, no código de exibição):

<% @models.each do |v| %>
  <li><%= h v.to_s %></li>
<% end %>
bhousel
fonte
Obrigado bhousel. Eu originalmente segui esse estilo de abordagem, mas acabei usando a solução que Vincent postou acima, pois significava que eu não precisava "modelar" o nome do arquivo (por exemplo, retire qualquer _, coloque em maiúscula! Cada palavra e depois entre eles de novo).
7282 mr_urf
com subdiretórios:...'/app/models/**/*.rb'
artemave 16/03/11
Object.subclasses_of foi descontinuado após a v2.3.8.
David J.
11

Em uma linha: Dir['app/models/\*.rb'].map {|f| File.basename(f, '.*').camelize.constantize }

vjt
fonte
7
Esse é bom, já que, no Rails 3, seus modelos não são carregados automaticamente por padrão; portanto, muitos dos métodos acima não retornam todos os modelos possíveis. Minha permutação também captura modelos em plugins e subdiretórios:Dir['**/models/**/*.rb'].map {|f| File.basename(f, '.*').camelize.constantize }
wbharding
2
@wbharding Isso é muito bom, mas ocorre um erro ao tentar constanteizar os nomes dos meus testes de modelo rspec. ;-)
Ajedi32
@wbharding solução agradável, mas ele quebra modelos quando você namespaced
Marcus Mansur
10

ActiveRecord::Base.connection.tables

Mark Locklear
fonte
Também um bom acompanhamento é <table_name> .column_names para listar todas as colunas na tabela. Portanto, para sua tabela de usuário você deve executar User.column_names
Mark Locklear
Isso fornecerá todas as tabelas, não apenas os modelos, pois algumas tabelas nem sempre têm modelos associados.
precisa saber é
7

Em apenas uma linha:

 ActiveRecord::Base.subclasses.map(&:name)
Adrian
fonte
2
Isso não mostra todos os modelos para mim. Não sei por que. É um par curto, na verdade.
precisa saber é
1
trabalhou para mim. É um pouco tarde para responder. Dá tempo a isso.
boulder_ruby
2
Provavelmente é necessário Rails.application.eager_load!antes da execução no modo de desenvolvimento.
Denis.peplin 23/09/2015
7

Ainda não posso comentar, mas acho que a resposta sj26 deve ser a resposta principal. Apenas uma dica:

Rails.application.eager_load! unless Rails.configuration.cache_classes
ActiveRecord::Base.descendants
panteo
fonte
6

Com o Rails 6 , o Zetiwerk se tornou o carregador de código padrão.

Para carregamento rápido, tente:

Zeitwerk::Loader.eager_load_all

Então

ApplicationRecord.descendants
demir
fonte
5

Sim, existem muitas maneiras de encontrar todos os nomes de modelos, mas o que eu fiz na minha gema model_info é que ele fornecerá todos os modelos incluídos nas gemas.

array=[], @model_array=[]
Rails.application.eager_load!
array=ActiveRecord::Base.descendants.collect{|x| x.to_s if x.table_exists?}.compact
array.each do |x|
  if  x.split('::').last.split('_').first != "HABTM"
    @model_array.push(x)
  end
  @model_array.delete('ActiveRecord::SchemaMigration')
end

então simplesmente imprima isso

@model_array
nitanshu verma
fonte
3

Isso funciona para o Rails 3.2.18

Rails.application.eager_load!

def all_models
  models = Dir["#{Rails.root}/app/models/**/*.rb"].map do |m|
    m.chomp('.rb').camelize.split("::").last
  end
end
ryan0
fonte
tensão para esse Rails.application.eager_load! ideia
equivalente8
3

Para evitar pré-carregar todos os Rails, você pode fazer o seguinte:

Dir.glob("#{Rails.root}/app/models/**/*.rb").each {|f| require_dependency(f) }

require_dependency (f) é o mesmo que Rails.application.eager_load!usa. Isso deve evitar erros de arquivo já necessários.

Em seguida, você pode usar todo tipo de solução para listar modelos de AR, como ActiveRecord::Base.descendants

John Owen Chile
fonte
2
Module.constants.select { |c| (eval c).is_a?(Class) && (eval c) < ActiveRecord::Base }
Naveed
fonte
lança TypeError: nenhuma conversão implícita de Symbol em String no console.
snowangel
1

Aqui está uma solução que foi examinada com um aplicativo Rails complexo (aquele que alimenta o Square)

def all_models
  # must eager load all the classes...
  Dir.glob("#{RAILS_ROOT}/app/models/**/*.rb") do |model_path|
    begin
      require model_path
    rescue
      # ignore
    end
  end
  # simply return them
  ActiveRecord::Base.send(:subclasses)
end

Ele pega as melhores partes das respostas neste tópico e as combina na solução mais simples e completa. Este identificador de casos em que seus modelos estão em subdiretórios, use set_table_name etc.

Pascal-Louis Perez
fonte
1

Acabei de encontrar este, pois preciso imprimir todos os modelos com seus atributos (baseados no comentário de @Aditya Sanghi):

ActiveRecord::Base.connection.tables.map{|x|x.classify.safe_constantize}.compact.each{ |model| print "\n\n"+model.name; model.new.attributes.each{|a,b| print "\n#{a}"}}
gouravtiwari21
fonte
1

Isso funcionou para mim. Agradecimentos especiais a todas as postagens acima. Isso deve retornar uma coleção de todos os seus modelos.

models = []

Dir.glob("#{Rails.root}/app/models/**/*.rb") do |model_path|
  temp = model_path.split(/\/models\//)
  models.push temp.last.gsub(/\.rb$/, '').camelize.constantize rescue nil
end
Kevin
fonte
1

Ele Railsimplementa o método descendants, mas os modelos nem sempre herdam ActiveRecord::Base, por exemplo, a classe que inclui o módulo ActiveModel::Modelterá o mesmo comportamento que um modelo, apenas não será vinculado a uma tabela.

Então, complementando o que dizem os colegas acima, o menor esforço faria o seguinte:

Patch de Macacos da classe ClassRuby:

class Class
  def extends? constant
    ancestors.include?(constant) if constant != self
  end
end

e o método models , incluindo ancestrais, como este:

O método Module.constantsretorna (superficialmente) uma coleção de symbols, em vez de constantes, portanto, o método Array#selectpode ser substituído como este patch de macaco do Module:

class Module

  def demodulize
    splitted_trail = self.to_s.split("::")
    constant = splitted_trail.last

    const_get(constant) if defines?(constant)
  end
  private :demodulize

  def defines? constant, verbose=false
    splitted_trail = constant.split("::")
    trail_name = splitted_trail.first

    begin
      trail = const_get(trail_name) if Object.send(:const_defined?, trail_name)
      splitted_trail.slice(1, splitted_trail.length - 1).each do |constant_name|
        trail = trail.send(:const_defined?, constant_name) ? trail.const_get(constant_name) : nil
      end
      true if trail
    rescue Exception => e
      $stderr.puts "Exception recovered when trying to check if the constant \"#{constant}\" is defined: #{e}" if verbose
    end unless constant.empty?
  end

  def has_constants?
    true if constants.any?
  end

  def nestings counted=[], &block
    trail = self.to_s
    collected = []
    recursivityQueue = []

    constants.each do |const_name|
      const_name = const_name.to_s
      const_for_try = "#{trail}::#{const_name}"
      constant = const_for_try.constantize

      begin
        constant_sym = constant.to_s.to_sym
        if constant && !counted.include?(constant_sym)
          counted << constant_sym
          if (constant.is_a?(Module) || constant.is_a?(Class))
            value = block_given? ? block.call(constant) : constant
            collected << value if value

            recursivityQueue.push({
              constant: constant,
              counted: counted,
              block: block
            }) if constant.has_constants?
          end
        end
      rescue Exception
      end

    end

    recursivityQueue.each do |data|
      collected.concat data[:constant].nestings(data[:counted], &data[:block])
    end

    collected
  end

end

Remendo de macaco String.

class String
  def constantize
    if Module.defines?(self)
      Module.const_get self
    else
      demodulized = self.split("::").last
      Module.const_get(demodulized) if Module.defines?(demodulized)
    end
  end
end

E, finalmente, o método dos modelos

def models
  # preload only models
  application.config.eager_load_paths = model_eager_load_paths
  application.eager_load!

  models = Module.nestings do |const|
    const if const.is_a?(Class) && const != ActiveRecord::SchemaMigration && (const.extends?(ActiveRecord::Base) || const.include?(ActiveModel::Model))
  end
end

private

  def application
    ::Rails.application
  end

  def model_eager_load_paths
    eager_load_paths = application.config.eager_load_paths.collect do |eager_load_path|
      model_paths = application.config.paths["app/models"].collect do |model_path|
        eager_load_path if Regexp.new("(#{model_path})$").match(eager_load_path)
      end
    end.flatten.compact
  end
rplaurindo
fonte
1
Dir.foreach("#{Rails.root.to_s}/app/models") do |model_path|
  next unless model_path.match(/.rb$/)
  model_class = model_path.gsub(/.rb$/, '').classify.constantize
  puts model_class
end

Isso fornecerá a você todas as classes de modelo que você tem no seu projeto.

Vencedor
fonte
0
def load_models_in_development
  if Rails.env == "development"
    load_models_for(Rails.root)
    Rails.application.railties.engines.each do |r|
      load_models_for(r.root)
    end
  end
end

def load_models_for(root)
  Dir.glob("#{root}/app/models/**/*.rb") do |model_path|
    begin
      require model_path
    rescue
      # ignore
    end
  end
end
Abdul
fonte
0

Eu tentei muitas dessas respostas sem sucesso no Rails 4 (uau, elas mudaram uma coisa ou duas por amor de Deus) e decidi adicionar as minhas. Aqueles que chamaram ActiveRecord :: Base.connection e puxaram os nomes das tabelas funcionaram, mas não obtiveram o resultado desejado, porque eu ocultei alguns modelos (em uma pasta dentro de app / models /) que eu não queria excluir:

def list_models
  Dir.glob("#{Rails.root}/app/models/*.rb").map{|x| x.split("/").last.split(".").first.camelize}
end

Coloquei isso em um inicializador e posso chamá-lo de qualquer lugar. Impede o uso desnecessário do mouse.

boulder_ruby
fonte
0

pode verificar isso

@models = ActiveRecord::Base.connection.tables.collect{|t| t.underscore.singularize.camelize}
Arvind
fonte
0

Supondo que todos os modelos estejam em app / models e você tenha grep & awk no seu servidor (na maioria dos casos),

# extract lines that match specific string, and print 2nd word of each line
results = `grep -r "< ActiveRecord::Base" app/models/ | awk '{print $2}'`
model_names = results.split("\n")

É mais rápido do que Rails.application.eager_load!ou repetindo cada arquivo com Dir.

EDITAR:

A desvantagem desse método é que ele perde modelos herdados indiretamente do ActiveRecord (por exemplo FictionalBook < Book). O caminho mais certo é que Rails.application.eager_load!; ActiveRecord::Base.descendants.map(&:name), embora seja meio lento.

konyak
fonte
0

Estou apenas jogando este exemplo aqui, se alguém achar útil. A solução é baseada nesta resposta https://stackoverflow.com/a/10712838/473040 .

Digamos que você tenha uma coluna public_uidque é usada como um ID primário para o mundo exterior (você pode encontrar perguntas sobre por que você faria isso aqui )

Agora, digamos que você introduziu esse campo em vários modelos existentes e agora deseja gerar novamente todos os registros que ainda não foram definidos. Você pode fazer isso assim

# lib/tasks/data_integirity.rake
namespace :di do
  namespace :public_uids do
    desc "Data Integrity: genereate public_uid for any model record that doesn't have value of public_uid"
    task generate: :environment do
      Rails.application.eager_load!
      ActiveRecord::Base
        .descendants
        .select {|f| f.attribute_names.include?("public_uid") }
        .each do |m| 
          m.where(public_uid: nil).each { |mi| puts "Generating public_uid for #{m}#id #{mi.id}"; mi.generate_public_uid; mi.save }
      end 
    end 
  end 
end

agora você pode correr rake di:public_uids:generate

equivalente8
fonte