Is there a way to get a collection of all the Models in your Rails app?

Viewed 123400

Is there a way that you can get a collection of all of the Models in your Rails app?

Basically, can I do the likes of: -

Models.each do |model|
  puts model.class.name
end
29 Answers

Just in case anyone stumbles on this one, I've got another solution, not relying on dir reading or extending the Class class...

ActiveRecord::Base.send :subclasses

This will return an array of classes. So you can then do

ActiveRecord::Base.send(:subclasses).map(&:name)

EDIT: Look at the comments and other answers. There are smarter answers than this one! Or try to improve this one as community wiki.

Models do not register themselves to a master object, so no, Rails does not have the list of models.

But you could still look in the content of the models directory of your application...

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

EDIT: Another (wild) idea would be to use Ruby reflection to search for every classes that extends ActiveRecord::Base. Don't know how you can list all the classes though...

EDIT: Just for fun, I found a way to list all classes

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

EDIT: Finally succeeded in listing all models without looking at directories

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

If you want to handle derived class too, then you will need to test the whole superclass chain. I did it by adding a method to the Class 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

With Rails 6, Zetiwerk became the default code loader.

For eager loading, try:

Zeitwerk::Loader.eager_load_all

Then

ApplicationRecord.descendants

This seems to work for me:

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

Rails only loads models when they are used, so the Dir.glob line "requires" all the files in the models directory.

Once you have the models in an array, you can do what you were thinking (e.g. in view code):

<% @models.each do |v| %>
  <li><%= h v.to_s %></li>
<% end %>

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

ActiveRecord::Base.connection.tables

I can't comment yet, but I think sj26 answer should be the top answer. Just a hint:

Rails.application.eager_load! unless Rails.configuration.cache_classes
ActiveRecord::Base.descendants
Module.constants.select { |c| (eval c).is_a?(Class) && (eval c) < ActiveRecord::Base }
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

This will give to you all the model classes you have on your project.

This worked for me. Special thanks to all the posts above. This should return a collection of all your models.

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

I've tried so many of these answers unsuccessfully in Rails 4 (wow they changed a thing or two for god sakes) I decided to add my own. The ones that called ActiveRecord::Base.connection and pulled the table names worked but didn't get the result I wanted because I've hidden some models (in a folder inside of app/models/) that I didn't want to delete:

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

I put that in an initializer and can call it from anywhere. Prevents unnecessary mouse-usage.

The Rails implements the method descendants, but models not necessarily ever inherits from ActiveRecord::Base, for example, the class that includes the module ActiveModel::Model will have the same behavior as a model, just doesn't will be linked to a table.

So complementing what says the colleagues above, the slightest effort would do this:

Monkey Patch of class Class of the Ruby:

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

and the method models, including ancestors, as this:

The method Module.constants returns (superficially) a collection of symbols, instead of constants, so, the method Array#select can be substituted like this monkey patch of the 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

Monkey patch of 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

And, finally, the models method

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

Make sure to eager load your app before calling descendants so that all the classes are loaded:

Rails.application.eager_load! unless Rails.application.config.eager_load

ApplicationRecord.descendants.each do |clazz|
  # do something with clazz, e.g. User, Event, Attendance, etc.
end

I'm just throwing this example here if anyone finds it useful. Solution is based on this answer https://stackoverflow.com/a/10712838/473040.

Let say you have a column public_uid that is used as a primary ID to outside world (you can findjreasons why you would want to do that here)

Now let say you've introduced this field on bunch of existing Models and now you want to regenerate all the records that are not yet set. You can do that like this

# 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

you can now run rake di:public_uids:generate

Related