¿Hay alguna manera de obtener una colección de todos los modelos en su aplicación Rails?

201

¿Hay alguna manera de que puedas obtener una colección de todos los Modelos en tu aplicación Rails?

Básicamente, ¿puedo hacer los gustos de: -

Models.each do |model|
  puts model.class.name
end
mr_urf
fuente
1
Si necesita recolectar todos los modelos, incluidos los modelos de motores Rails / railties, vea la respuesta de @jaime
Andrei
No funciona en rieles 5.1
aks

Respuestas:

98

EDITAR: mira los comentarios y otras respuestas. ¡Hay respuestas más inteligentes que esta! O intente mejorar este como wiki comunitario.

Los modelos no se registran en un objeto maestro, por lo que no, Rails no tiene la lista de modelos.

Pero aún puede buscar en el contenido del directorio de modelos de su aplicación ...

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

EDITAR: Otra idea (salvaje) sería usar la reflexión Ruby para buscar todas las clases que extiendan ActiveRecord :: Base. Sin embargo, no sé cómo puedes enumerar todas las clases ...

EDITAR: solo por diversión, encontré una manera de enumerar todas las clases

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

EDITAR: Finalmente logró enumerar todos los modelos sin mirar directorios

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

Si también desea manejar la clase derivada, deberá probar toda la cadena de superclase. Lo hice agregando un método a la clase 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
fuente
66
FYI, cronometré ambos métodos solo por diversión. Buscar los directorios es un orden de magnitud más rápido que buscar en las clases. Eso probablemente fue obvio, pero ahora lo sabes :)
Edward Anderson
9
Además, es importante tener en cuenta que la búsqueda de modelos a través de los métodos de constantes no incluirá nada a lo que no se haya hecho referencia desde que se inició la aplicación, ya que solo carga los modelos a pedido.
Edward Anderson
44
Prefiero 'Kernel.const_get constant_name' a 'eval constant_name'.
Jeremy Weathers
3
RAILS_ROOTya no está disponible en Rails 3. En su lugar, useDir.glob(Rails.root.join('app/models/*'))
fanaugen
1
En realidad, los modelos se registran a sí mismos como descendientes de ActiveRecord::Baseahora, por lo que si desea cargar todos los modelos, puede iterarlos fácilmente; vea mi respuesta a continuación.
sj26
393

La respuesta completa para los rieles 3, 4 y 5 es:

Si cache_classesestá desactivado (por defecto está desactivado en desarrollo, pero activado en producción):

Rails.application.eager_load!

Luego:

ActiveRecord::Base.descendants

Esto asegura que todos los modelos en su aplicación, independientemente de dónde se encuentren, se carguen y las gemas que está utilizando que proporcionan modelos también se cargan.

Esto también debería funcionar en clases que heredan ActiveRecord::Base, como ApplicationRecorden Rails 5, y devuelven solo ese subárbol de descendientes:

ApplicationRecord.descendants

Si desea saber más sobre cómo se hace esto, consulte ActiveSupport :: DescendantsTracker .

sj26
fuente
33
¡Increíble! Esta debería ser la respuesta aceptada. Para cualquiera que use esto en una tarea de rastrillo: haga que su tarea dependa :environmentpara eager_load!que funcione.
Jo Liss
1
O, como una alternativa un poco más rápida Rails.application.eager_load!, simplemente puede cargar los modelos:Dir.glob(Rails.root.join('app/models/*')).each do |x| require x end
Ajedi32
55
@ Ajedi32 que no está completo, los modelos se pueden definir fuera de esos directorios, especialmente cuando se usan motores con modelos. Un poco mejor, al menos glob todos los Rails.paths["app/models"].existentdirectorios. La ansiosa carga de toda la aplicación es una respuesta más completa y se asegurará de que no quede absolutamente ningún lugar para definir los modelos.
sj26
2
Comprendí lo que sj26 significa, pero tal vez hay un pequeño error: por lo que sé en el entorno de desarrollo, cache_classes está desactivado (falso), por eso es necesario cargar manualmente la aplicación para acceder a todos los modelos. explicado aquí
masciugo
3
@ Ajedi32 nuevamente, no es la respuesta completa. Si desea cargar solo modelos ansiosos, intente:Rails.application.paths["app/models"].eager_load!
sj26
119

En caso de que alguien tropiece con este, tengo otra solución, no confiar en la lectura de directorios o extender la clase Class ...

ActiveRecord::Base.send :subclasses

Esto devolverá una variedad de clases. Entonces puedes hacer

ActiveRecord::Base.send(:subclasses).map(&:name)
kikito
fuente
8
¿Por qué no usas ActiveRecord::Base.subclassespero tienes que usar send? Además, parece que tiene que "tocar" el modelo antes de que aparezca, por ejemplo, c = Category.newy aparecerá. De lo contrario, no lo hará.
nonopolaridad
52
En Rails 3, esto se ha cambiado aActiveRecord::Base.descendants
Tobias Cohen
3
Debe usar "enviar" porque el miembro: subclasses está protegido.
Kevin Rood
11
Gracias por el consejo de Rails 3. Para cualquier otra persona que venga, aún debe "tocar" los modelos antes de ActiveRecord::Base.descendantsenumerarlos.
nfm
3
Técnicamente en Rails 3 tienes subclases y descendientes, significan cosas diferentes.
sj26
67
ActiveRecord::Base.connection.tables.map do |model|
  model.capitalize.singularize.camelize
end

volverá

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

Información adicional Si desea llamar a un método en el nombre del objeto sin modelo: método desconocido de cadena o errores variables, use esto

model.classify.constantize.attribute_names
lightyrs
fuente
8
Sin embargo, esto le dará todas las tablas, no solo los modelos, ya que algunas tablas no siempre tienen modelos asociados.
courtimas
Esta respuesta debe considerarse incorrecta ya que es factible (y común en configuraciones heredadas) configurar el nombre de la tabla para que sea algo más que el nombre pluralizado del modelo. Esta respuesta da la respuesta correcta incluso cuando la configuración se desvía de la configuración predeterminada.
lorefnon
en algunos casos esto funciona mejor que ActiveRecord::Base.send :subclasses: buscar los nombres de las tablas es una buena idea. Generar automáticamente los nombres de los modelos puede ser problemático como se menciona anteriormente.
Tilo
.capitalize.singularize.camelizepuede ser reemplazado a .classify.
Maxim
34

Busqué formas de hacer esto y terminé eligiendo de esta manera:

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

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

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

jaime
fuente
1
Esta es la única forma en que puedo obtener TODOS los modelos, incluidos los modelos de motores Rails utilizados en la aplicación. ¡Gracias por el consejo!
Andrei
2
Algunos métodos útiles: ActiveRecord::Base.connection.tables.each{|t| begin puts "%s: %d" % [t.humanize, t.classify.constantize.count] rescue nil end}algunos de los modelos pueden no estar activados, por lo tanto, debe rescatarlo.
Andrei
2
Adaptando @ Andrei's un poco: model_classes = ActiveRecord::Base.connection.tables.collect{|t| t.classify.constantize rescue nil }.compact
Max Williams
30

Para Rails5, los modelos ahora son subclases de ApplicationRecordmodo que para obtener una lista de todos los modelos en su aplicación, haga lo siguiente:

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

O más corto:

ApplicationRecord.descendants.collect(&:name)

Si está en modo de desarrollo, necesitará cargar modelos antes de:

Rails.application.eager_load!
Nimir
fuente
1
Supongo que esto requeriría que las clases ya estén cargadas y daría resultados incompletos en el entorno de desarrollo con la carga automática habilitada. No votaré negativamente, pero tal vez esto debería mencionarse en la respuesta.
lorefnon
tarifa suficiente, actualización
Nimir
Estoy en Rails 6.0.2 y el eager_load! no hizo que el método descendiente devolviera nada más que una matriz vacía.
jgomo3
23

Creo que la solución de @ hnovick es genial si no tienes modelos sin mesa. Esta solución también funcionaría en modo de desarrollo

Sin embargo, mi enfoque es sutilmente diferente:

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

Se supone que classify te da el nombre de la clase de una cadena correctamente . safe_constantize asegura que puede convertirlo en una clase de forma segura sin lanzar una excepción. Esto es necesario en caso de que tenga tablas de base de datos que no sean modelos. compacto para que se eliminen los nulos en la enumeración.

Aditya Sanghi
fuente
3
Eso es increíble @Aditya Sanghi. No sabía acerca safe_constantize.
lightyrs
Para los carriles 2.3.x, use: ActiveRecord :: Base.connection.tables.map {| x | x.classify.constantize rescue nil} .compact
iheggie
@iheggie Generalmente es mejor publicar eso como una respuesta separada que editarlo en la publicación existente.
Pokechu22
gracias, encontré tu respuesta más adecuada para mí #adiya
ilusionista
21

Si solo quieres los nombres de clase:

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

Simplemente ejecútalo en la consola de Rails, nada más. ¡Buena suerte!

EDITAR: @ ​​sj26 es correcto, primero debe ejecutar esto antes de poder llamar a los descendientes:

Rails.application.eager_load!
Jordan Michael Rushing
fuente
Justo lo que quería. ¡Gracias!
Sunsations
llamando mapcon puts? No entiendo el punto debería serActiveRecord::Base.descendants.map(&:model_name)
Nuno Costa
Puede hacerlo de esa manera, pero estarán en una sola matriz, en lugar de línea por línea, en un formato mucho más fácil de leer.
Jordan Michael Rushing
17

Esto parece funcionar para mí:

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

Rails solo carga modelos cuando se usan, por lo que la línea Dir.glob "requiere" todos los archivos en el directorio de modelos.

Una vez que tenga los modelos en una matriz, puede hacer lo que estaba pensando (por ejemplo, en el código de vista):

<% @models.each do |v| %>
  <li><%= h v.to_s %></li>
<% end %>
bhousel
fuente
Gracias bhousel. Originalmente seguí con este estilo de enfoque, pero terminé usando la solución que Vincent publicó anteriormente, ya que significaba que no tenía que "Modelar" el nombre del archivo también (es decir, eliminar cualquier _, poner en mayúscula! Cada palabra y luego unirme ellos de nuevo).
mr_urf
con subdirectorios:...'/app/models/**/*.rb'
artemave
Object.subclasses_of está en desuso después de v2.3.8.
David J.
11

En una línea: Dir['app/models/\*.rb'].map {|f| File.basename(f, '.*').camelize.constantize }

vjt
fuente
77
Este es bueno ya que, en Rails 3, sus modelos no se cargan automáticamente por defecto, por lo que muchos de los métodos anteriores no devolverán todos los modelos posibles. Mi permutación también captura modelos en complementos y subdirectorios:Dir['**/models/**/*.rb'].map {|f| File.basename(f, '.*').camelize.constantize }
wbharding
2
@wbharding Eso es bastante bueno, pero se equivoca cuando intenta constatar los nombres de mis pruebas de modelo rspec. ;-)
Ajedi32
@wbharding buena solución pero se rompe cuando tienes modelos con espacios de nombres
Marcus Mansur
10

ActiveRecord::Base.connection.tables

Mark Locklear
fuente
También un buen seguimiento es <table_name> .column_names para enumerar todas las columnas de la tabla. Entonces, para su tabla de usuario, ejecutaría User.column_names
Mark Locklear
Sin embargo, esto le dará todas las tablas, no solo los modelos, ya que algunas tablas no siempre tienen modelos asociados.
courtimas
7

En solo una línea:

 ActiveRecord::Base.subclasses.map(&:name)
Adrian
fuente
2
Eso no muestra todos los modelos para mí. No estoy seguro de por qué. Es un par corto, de hecho.
courtimas
1
trabajó para mi. 'solo un poco tarde para responder eso es todo. Dale tiempo.
boulder_ruby
2
Probablemente sea necesario Rails.application.eager_load!antes de la ejecución en modo de desarrollo.
denis.peplin
7

Todavía no puedo comentar, pero creo que la respuesta sj26 debería ser la respuesta principal. Solo una pista:

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

Con Rails 6 , Zetiwerk convirtió en el cargador de código predeterminado.

Para una carga ansiosa, intente:

Zeitwerk::Loader.eager_load_all

Luego

ApplicationRecord.descendants
demir
fuente
5

Sí, hay muchas formas de encontrar todos los nombres de modelos, pero lo que hice en mi gema model_info es que te dará todos los modelos incluso incluidos en las 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

entonces simplemente imprima esto

@model_array
nitanshu verma
fuente
3

Esto funciona para 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
fuente
upvolt para que Rails.application.eager_load! idea
equivalente8
3

Para evitar la carga previa de todos los rieles, puede hacer esto:

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

require_dependency (f) es lo mismo que Rails.application.eager_load! usa. Esto debería evitar errores de archivo ya requeridos.

Luego puede usar todo tipo de soluciones para enumerar modelos AR, como ActiveRecord::Base.descendants

John Owen Chile
fuente
2
Module.constants.select { |c| (eval c).is_a?(Class) && (eval c) < ActiveRecord::Base }
Naveed
fuente
lanza TypeError: no hay conversión implícita de Symbol en String en la consola.
snowangel
1

Aquí hay una solución que ha sido examinada con una aplicación Rails compleja (la que impulsa 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

Toma las mejores partes de las respuestas en este hilo y las combina en la solución más simple y completa. Este maneja casos donde sus modelos están en subdirectorios, use set_table_name, etc.

Pascal-Louis Perez
fuente
1

Acabo de encontrar este, ya que necesito imprimir todos los modelos con sus atributos (basados ​​en el comentario 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
fuente
1

Esto funcionó para mí. Un agradecimiento especial a todas las publicaciones anteriores. Esto debería devolver una colección de todos sus 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
fuente
1

Los Railsimplementa el método descendants, pero los modelos no necesariamente siempre hereda de ActiveRecord::Base, por ejemplo, la clase que incluye el móduloActiveModel::Model tendrá el mismo comportamiento que un modelo, simplemente no estarán vinculados a una mesa.

Entonces, complementando lo que dicen los colegas anteriores, el más mínimo esfuerzo haría esto:

Monkey Patch de la clase Classde Ruby:

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

y el método models, incluidos los ancestros, como este:

El método Module.constantsdevuelve (superficialmente) una colección de symbols, en lugar de constantes, por lo que el método Array#selectpuede sustituirse como este parche de mono de 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

Parche de mono de 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

Y, finalmente, el método de los 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
fuente
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

Esto te dará todas las clases de modelos que tienes en tu proyecto.

Víctor
fuente
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
fuente
0

He intentado muchas de estas respuestas sin éxito en Rails 4 (wow, cambiaron una o dos cosas por el amor de Dios) decidí agregar la mía. Los que llamaron a ActiveRecord :: Base.connection y obtuvieron los nombres de las tablas funcionaron pero no obtuvieron el resultado que quería porque escondí algunos modelos (en una carpeta dentro de app / models /) que no quería Eliminar:

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

Lo puse en un inicializador y puedo llamarlo desde cualquier lugar. Previene el uso innecesario del mouse.

boulder_ruby
fuente
0

puede verificar esto

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

Suponiendo que todos los modelos están en la aplicación / modelos y que tiene grep & awk en su servidor (la mayoría de los 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")

Es más rápido Rails.application.eager_load!o recorre cada archivo con Dir.

EDITAR:

La desventaja de este método es que pierde modelos que heredan indirectamente de ActiveRecord (por ejemplo FictionalBook < Book). La forma más segura es Rails.application.eager_load!; ActiveRecord::Base.descendants.map(&:name), aunque sea un poco lenta.

Konyak
fuente
0

Solo estoy lanzando este ejemplo aquí si alguien lo encuentra útil. La solución se basa en esta respuesta https://stackoverflow.com/a/10712838/473040 .

Digamos que tiene una columna public_uidque se utiliza como ID principal para el mundo exterior (puede encontrar las razones por las que desea hacer eso aquí )

Ahora supongamos que ha introducido este campo en un montón de modelos existentes y ahora desea regenerar todos los registros que aún no se han establecido. Puedes hacer eso así

# 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

ahora puedes correr rake di:public_uids:generate

equivalente8
fuente