有没有办法在Rails应用程序中收集所有模型?


201

有没有办法让您在Rails应用程序中收集所有模型?

基本上,我可以做以下事情吗?

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

1
如果您需要收集所有模型,包括Rails引擎/轨道模型,请参阅@jaime的答案
Andrei

在轨道上不起作用5.1
AKS

Answers:


98

编辑:看评论和其他答案。有比这更聪明的答案!或尝试将其改进为社区Wiki。

模型不会将自己注册到主对象,因此,Rails没有模型列表。

但是您仍然可以在应用程序的models目录中查找内容。

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

编辑:另一个(疯狂的)想法是使用Ruby反射来搜索扩展ActiveRecord :: Base的每个类。虽然不知道如何列出所有课程...

编辑:只是为了好玩,我找到了列出所有课程的方法

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

编辑:最终成功列出所有模型,而无需查看目录

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

如果您也想处理派生类,那么您将需要测试整个超类链。我通过向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

6
仅供参考,我只是出于娱乐目的而对这两种方法进行了计时。查找目录比通过类查找要快一个数量级。这可能很明显,但现在您知道了:)
爱德华·安德森

9
另外,请务必注意,通过常量方法搜索模型将不包括自应用程序启动以来未引用的任何内容,因为它仅按需加载模型。
爱德华·安德森

4
我更喜欢'Kernel.const_get constant_name'而不是'eval constant_name'。
杰里米·韦瑟斯

3
RAILS_ROOT在Rails 3中不再可用。而是使用Dir.glob(Rails.root.join('app/models/*'))
fanaugen

1
实际上,这些模型确实会将自己注册为ActiveRecord::Base现在的后代,因此,如果您渴望加载所有模型,则可以轻松地对其进行迭代-请参见下面的答案。
sj26 '20年

393

Rails 3、4和5的全部答案是:

如果cache_classes处于关闭状态(默认情况下,它在开发中处于关闭状态,但在生产中处于打开状态):

Rails.application.eager_load!

然后:

ActiveRecord::Base.descendants

这样可以确保应用程序中的所有模型(无论它们在何处)都被加载,并且还可以加载您使用的提供模型的所有gem。

这也应该适用于继承自的类ActiveRecord::Base,例如ApplicationRecord在Rails 5中,并且仅返回后代的子树:

ApplicationRecord.descendants

如果您想了解更多关于如何做到这一点,检查出的ActiveSupport :: DescendantsTracker


33
太棒了!这应该是公认的答案。对于rake任务使用此人:让你的任务依赖:environmenteager_load!工作。
2012年

1
或者,作为稍微快一点的替代方法Rails.application.eager_load!,您可以加载模型:Dir.glob(Rails.root.join('app/models/*')).each do |x| require x end
Ajedi32

5
@ Ajedi32并不完整,可以在这些目录之外定义模型,尤其是在将引擎与模型一起使用时。稍好一点,至少可以遍历所有Rails.paths["app/models"].existent目录。急于加载整个应用程序是一个更完整的答案,并将确保绝对没有地方可以定义模型。
sj26

2
我明白了sj26的意思,但也许有一个小错误:据我所知,在开发环境中,cache_classes已关闭(false),这就是为什么您需要手动急于加载应用程序以访问所有模型的原因。在这里解释
masciugo 2013年

3
再次@ Ajedi32,不是完整的答案。如果您只想加载模型,请尝试:Rails.application.paths["app/models"].eager_load!
sj26 2013年

119

万一有人偶然发现这个,我有另一种解决方案,不依靠dir读取或扩展Class类。

ActiveRecord::Base.send :subclasses

这将返回一个类数组。所以你可以做

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

8
为什么不使用ActiveRecord::Base.subclasses却必须使用send?同样,例如,您似乎必须“触摸”模型,然后模型c = Category.new才会显示出来。否则,不会。
极性

52
在Rails 3中,此ActiveRecord::Base.descendants
字段

3
您必须使用“ send”,因为:subclasses成员受到保护。
凯文·罗德

11
感谢Rails 3技巧。对于其他任何人,您仍然需要“触摸”模型,然后ActiveRecord::Base.descendants才能列出它们。
nfm

3
从技术上讲,在Rails 3中,您具有子类后代,它们的含义不同。
sj26

67
ActiveRecord::Base.connection.tables.map do |model|
  model.capitalize.singularize.camelize
end

将返回

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

附加信息如果要在对象名称上调用方法而不使用model:string未知方法或变量错误,请使用此方法

model.classify.constantize.attribute_names

8
但是,这将为您提供所有表,而不仅仅是模型,因为某些表并不总是具有关联的模型。
courtsimas 2012年

该答案应被认为是不正确的,因为将表的名称配置为不同于模型的复数名称是可行的(在旧式设置中很常见)。即使设置偏离默认配置,此答案也会提供正确的答案。
lorefnon

在某些情况下,这比ActiveRecord::Base.send :subclasses- 更好地使用-查找表名是一个好主意。如lorefnon所述,自动生成模型名称可能会出现问题。
Tilo

.capitalize.singularize.camelize可以替换为.classify
Maxim

34

我寻找了实现此目的的方法,最终选择了这种方法:

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

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

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


1
这是获取所有模型(包括应用程序中使用的Rails引擎模型)的唯一方法。谢谢你的提示!
安德烈

2
一些有用的方法:ActiveRecord::Base.connection.tables.each{|t| begin puts "%s: %d" % [t.humanize, t.classify.constantize.count] rescue nil end}某些模型可能未激活,因此您需要进行补救。
Andrei

2
改编@Andrei有点: model_classes = ActiveRecord::Base.connection.tables.collect{|t| t.classify.constantize rescue nil }.compact
Max Williams

30

现在,对于Rails5模型是的子类ApplicationRecord要获取您应用中所有模型的列表,请执行以下操作:

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

或更短:

ApplicationRecord.descendants.collect(&:name)

如果您处于开发人员模式,则需要在以下情况下急于加载模型:

Rails.application.eager_load!

1
我认为这将要求已加载类,并且在启用自动加载的开发环境中给出的结果不完整。我不会拒绝投票,但也许应该在答案中提及。
lorefnon

票价足够更新
Nimir

我在Rails 6.0.2和eager_load上!没有使后代方法返回空数组以外的任何东西。
jgomo3

23

我认为@hnovick的解决方案很不错,如果您没有无表模型。该解决方案也将在开发模式下工作

我的方法虽然有所不同-

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

classify应该从字符串中正确地给您类的名称。safe_constantize确保您可以安全地将其转换为类,而不会引发异常。如果您有不是模型的数据库表,则需要这样做。紧凑,以便删除枚举中的所有零。


3
真棒@Aditya Sanghi。我不知道safe_constantize
lightyrs 2012年

对于rails 2.3.x,请使用:ActiveRecord :: Base.connection.tables.map {| x | x.classify.constantize rescue nil} .compact
iheggie 2014年

@iheggie通常,将其作为单独的答案发布比将其编辑到现有帖子中更好。
Pokechu22年

谢谢,我发现您的答案最适合我#adiya
魔术师

21

如果只需要类名:

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

只需在Rails控制台中运行它,仅此而已。祝好运!

编辑:@ sj26是正确的,您需要先运行此命令,然后才能调用后代:

Rails.application.eager_load!

正是我想要的。谢谢!
sunsations

打电话mapputs?我不明白重点是ActiveRecord::Base.descendants.map(&:model_name)
Nuno Costa

您可以通过这种方式进行操作,但是它们将以更易于阅读的格式位于单个数组中,而不是逐行显示。
乔丹·迈克尔·拉什

17

这似乎为我工作:

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

Rails仅在使用模型时才加载它们,因此Dir.glob行“需要” models目录中的所有文件。

将模型放入数组后,您可以做您想做的事情(例如,在视图代码中):

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

谢谢bhousel。我最初采用这种方式,但最终使用了文森特在上面发布的解决方案,因为这意味着我也不必对文件名进行“模型化”(即,去除任何_,大写!每个单词然后加入)他们)。
mr_urf

带有子目录:...'/app/models/**/*.rb'
artemave 2011年

v2.3.8之后不推荐使用Object.subclasses_of
David J.

11

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


7
这很不错,因为在Rails 3中,默认情况下不会自动加载模型,因此上述许多方法都不会返回所有可能的模型。我的排列还捕获了插件和子目录中的模型:Dir['**/models/**/*.rb'].map {|f| File.basename(f, '.*').camelize.constantize }
wbharding 2011年

2
@wbharding很好,但是当它试图使我的rspec模型测试的名称保持不变时,它会出错。;-)
Ajedi32

@wbharding一个不错的解决方案,但是当您使用命名空间模型时,它会中断
Marcus Mansur

10

ActiveRecord::Base.connection.tables


还有一个不错的跟进是<table_name> .column_names,列出表中的所有列。因此,对于您的用户表,您将执行User.column_names
Mark Locklear,

但是,这将为您提供所有表,而不仅仅是模型,因为某些表并不总是具有关联的模型。
courtsimas 2013年

7

仅一行:

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

2
这并不代表我所有的模型。不知道为什么。实际上,这很短。
courtsimas

1
为我工作。回答所有问题就有点晚了。给它点时间。
boulder_ruby

2
Rails.application.eager_load!在开发模式下执行之前可能需要它。
denis.peplin 2015年

7

我还不能发表评论,但我认为sj26答案应该是最佳答案。只是一个提示:

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

6

随着轨道6Zetiwerk成为默认的代码加载器。

对于急切的加载,请尝试:

Zeitwerk::Loader.eager_load_all

然后

ApplicationRecord.descendants

5

是的,有很多方法可以找到所有模型名称,但是我在gem model_info中所做的是,它将为您提供甚至包括在gem中的所有模型。

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

然后简单地打印这个

@model_array

3

这适用于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

该Rails.application.eager_load!想法
等效

3

为了避免预加载所有Rails,可以执行以下操作:

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

require_dependency(f)与 Rails.application.eager_load!使用。这应该避免已经需要的文件错误。

然后,您可以使用各种解决方案来列出AR模型,例如 ActiveRecord::Base.descendants


2
Module.constants.select { |c| (eval c).is_a?(Class) && (eval c) < ActiveRecord::Base }

抛出TypeError:在控制台中没有将Symbol隐式转换为String的情况。
snowangel 2013年

1

这是一个经过复杂的Rails应用程序审核的解决方案(一个为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

它采用了该主题中答案的最佳部分,并将它们组合成最简单,最彻底的解决方案。这可以处理您的模型位于子目录中,使用set_table_name等的情况。


1

刚遇到这一点,因为我需要打印所有带有属性的模型(建立在@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}"}}

1

这对我有用。特别感谢以上所有职位。这应该返回所有模型的集合。

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

1

Rails实现实现的方法descendants,但模型不一定要继承自ActiveRecord::Base例如包含模块的类ActiveModel::Model将具有与模型相同的行为,只是不会链接到表。

因此,对上面的同事所说的做些补充,只要付出一点点努力就可以做到:

ClassRuby 类的Monkey Patch :

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

以及models包括祖先在内的方法,如下所示:

该方法Module.constants(表面上)返回的集合symbols,而不是常量,因此,Array#select可以像下面的猴子补丁那样替换该方法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

的猴子补丁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

最后,模型方法

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

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

这将为您提供项目中所有的模型类。


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

0

我在Rails 4中尝试了许多这样的答案,但都没有成功(哇,为了上帝的缘故,他们改变了一两件事),我决定添加自己的答案。那些调用ActiveRecord :: Base.connection并提取表名称的函数可以工作,但没有得到我想要的结果,因为我隐藏了一些我不想隐藏的模型(位于app / models /内的文件夹中)删除:

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

我把它放在一个初始化器中,可以在任何地方调用它。防止不必要的鼠标使用。


0

可以检查一下

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

0

假设所有模型都在应用程序/模型中,并且您的服务器上有grep&awk(大多数情况),

# 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")

它比使用Rails.application.eager_load!或循环遍历每个文件更快 Dir

编辑:

这种方法的缺点是它会丢失从ActiveRecord间接继承的模型(例如FictionalBook < Book)。最可靠的方法是Rails.application.eager_load!; ActiveRecord::Base.descendants.map(&:name),即使有点慢。


0

如果有人发现它有用,我只是在这里举这个例子。解决方案基于此答案https://stackoverflow.com/a/10712838/473040

假设您有一列public_uid用作外界的主要ID(您可以在这里找到为什么要这样做的理由)

现在,假设您在一堆现有模型上引入了此字段,现在您想重新生成所有尚未设置的记录。你可以这样

# 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

您现在可以运行 rake di:public_uids:generate

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.