Existe-t-il un moyen d'obtenir une collection de tous les modèles de votre application Rails?

201

Existe-t-il un moyen d'obtenir une collection de tous les modèles de votre application Rails?

Fondamentalement, puis-je faire des choses comme: -

Models.each do |model|
  puts model.class.name
end
mr_urf
la source
1
Si vous avez besoin de collecter tous les modèles, y compris les modèles de moteurs / rails Rails, voir la réponse de @jaime
Andrei
Ne fonctionne pas sur les rails 5.1
aks

Réponses:

98

EDIT: Regardez les commentaires et autres réponses. Il y a des réponses plus intelligentes que celle-ci! Ou essayez d'améliorer celui-ci en tant que wiki communautaire.

Les modèles ne s'enregistrent pas eux-mêmes dans un objet maître, donc non, Rails n'a pas la liste des modèles.

Mais vous pouvez toujours regarder dans le contenu du répertoire des modèles de votre application ...

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

EDIT: Une autre idée (sauvage) serait d'utiliser la réflexion Ruby pour rechercher toutes les classes qui étendent ActiveRecord :: Base. Je ne sais pas comment lister toutes les classes ...

EDIT: Juste pour le plaisir, j'ai trouvé un moyen de lister toutes les classes

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

EDIT: Enfin réussi à lister tous les modèles sans regarder les répertoires

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 vous souhaitez également gérer la classe dérivée, vous devrez tester l'ensemble de la chaîne de superclasse. Je l'ai fait en ajoutant une méthode à la 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
la source
6
Pour info, j'ai chronométré les deux méthodes juste pour le plaisir. La recherche dans les répertoires est un ordre de grandeur plus rapide que la recherche dans les classes. C'était probablement évident, mais maintenant vous savez :)
Edward Anderson
9
De plus, il est important de noter que la recherche de modèles via les méthodes de constantes n'inclura rien qui n'a pas été référencé depuis le démarrage de l'application, car elle ne charge les modèles qu'à la demande.
Edward Anderson
4
Je préfère «Kernel.const_get constant_name» à «eval constant_name».
Jeremy Weathers
3
RAILS_ROOTn'est plus disponible dans Rails 3. Au lieu de cela, utilisezDir.glob(Rails.root.join('app/models/*'))
fanaugen
1
En fait, les modèles s'enregistrent eux-mêmes en tant que descendants de ActiveRecord::Basemaintenant, donc si vous avez hâte de charger tous les modèles, vous pouvez les itérer facilement - voir ma réponse ci-dessous.
sj26
393

La réponse complète pour les rails 3, 4 et 5 est:

Si cache_classesest désactivé (par défaut, il est désactivé en développement, mais activé en production):

Rails.application.eager_load!

Ensuite:

ActiveRecord::Base.descendants

Cela garantit que tous les modèles de votre application, quel que soit leur emplacement, sont chargés et toutes les gemmes que vous utilisez qui fournissent des modèles sont également chargées.

Cela devrait également fonctionner sur les classes qui héritent de ActiveRecord::Base, comme ApplicationRecorddans Rails 5, et renvoyer uniquement ce sous-arbre de descendants:

ApplicationRecord.descendants

Si vous souhaitez en savoir plus sur la façon dont cela est fait, consultez ActiveSupport :: DescendantsTracker .

sj26
la source
33
Impressionnant! Cela devrait être la réponse acceptée. Pour tous ceux qui l'utilisent dans une tâche de râteau: faites en sorte que votre tâche dépende :environmentdu eager_load!fonctionnement.
Jo Liss
1
Ou, comme alternative légèrement plus rapide à Rails.application.eager_load!, vous pouvez simplement charger les modèles:Dir.glob(Rails.root.join('app/models/*')).each do |x| require x end
Ajedi32
5
@ Ajedi32 qui n'est pas complet, les modèles peuvent être définis en dehors de ces répertoires, en particulier lors de l'utilisation de moteurs avec des modèles. Un peu mieux, au moins glob tous les Rails.paths["app/models"].existentrépertoires. Désireux de charger toute l'application est une réponse plus complète et vous assurera qu'il n'y a absolument plus de place pour les modèles à définir.
sj26
2
J'ai compris ce que sj26 signifie, mais il y a peut-être une petite erreur: pour autant que je sache dans l'environnement de développement, cache_classes est désactivé (faux), c'est pourquoi vous devez charger manuellement l'application pour accéder à tous les modèles. expliqué ici
masciugo
3
@ Ajedi32 à nouveau, pas la réponse complète. Si vous souhaitez charger uniquement des modèles, essayez:Rails.application.paths["app/models"].eager_load!
sj26
119

Juste au cas où quelqu'un trébucherait sur celui-ci, j'ai une autre solution, ne pas compter sur la lecture de dir ou étendre la classe Class ...

ActiveRecord::Base.send :subclasses

Cela renverra un tableau de classes. Vous pouvez donc faire

ActiveRecord::Base.send(:subclasses).map(&:name)
kikito
la source
8
pourquoi n'utilisez-vous pas ActiveRecord::Base.subclassesmais devez utiliser send? De plus, il semble que vous devez "toucher" le modèle avant qu'il n'apparaisse, par exemple c = Category.newet qu'il apparaisse. Sinon, ce ne sera pas le cas.
polarité
52
Dans Rails 3, cela a été changé enActiveRecord::Base.descendants
Tobias Cohen
3
Vous devez utiliser "envoyer" car le membre: sous-classes est protégé.
Kevin Rood
11
Merci pour le conseil de Rails 3. Pour toute autre personne qui vient, vous devez toujours "toucher" les modèles avant de ActiveRecord::Base.descendantsles énumérer.
nfm
3
Techniquement, dans Rails 3, vous avez des sous-classes et des descendants, ils signifient des choses différentes.
sj26
67
ActiveRecord::Base.connection.tables.map do |model|
  model.capitalize.singularize.camelize
end

reviendra

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

Informations supplémentaires Si vous souhaitez appeler une méthode sur le nom de l'objet sans modèle: chaîne méthode inconnue ou erreurs de variable utilisez ceci

model.classify.constantize.attribute_names
lightyrs
la source
8
Cela vous permettra d'obtenir toutes les tables, pas seulement les modèles, car certaines tables n'ont pas toujours de modèles associés.
courtsimas
Cette réponse doit être considérée comme incorrecte car il est possible (et courant dans les configurations héritées) de configurer le nom de la table pour être autre chose que le nom pluralisé du modèle. Cette réponse donne la bonne réponse même lorsque la configuration s'écarte de la configuration par défaut.
lorefnon
dans certains cas, cela fonctionne mieux que ActiveRecord::Base.send :subclasses- rechercher les noms de table est une bonne idée. La génération automatique des noms de modèle peut être problématique, comme indiqué précédemment.
Tilo
.capitalize.singularize.camelizepeut être remplacé par .classify.
Maxim
34

J'ai cherché des moyens de le faire et j'ai fini par choisir de cette façon:

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

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

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

jaime
la source
1
C'est la seule façon d'obtenir TOUS les modèles, y compris les modèles de moteurs Rails utilisés dans l'application. Merci pour le conseil!
Andrei
2
Quelques méthodes utiles: ActiveRecord::Base.connection.tables.each{|t| begin puts "%s: %d" % [t.humanize, t.classify.constantize.count] rescue nil end}Certains modèles peuvent ne pas être activés, vous devez donc le sauver.
Andrei
2
S'adapter un peu à Andrei: model_classes = ActiveRecord::Base.connection.tables.collect{|t| t.classify.constantize rescue nil }.compact
Max Williams
30

Pour les modèles Rails5 sont maintenant des sous - classes de ApplicationRecordafin d'obtenir la liste de tous les modèles dans votre application, vous faites:

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

Ou plus court:

ApplicationRecord.descendants.collect(&:name)

Si vous êtes en mode dev, vous devrez charger les modèles avant:

Rails.application.eager_load!
Nimir
la source
1
Je suppose que cela nécessiterait que les classes soient déjà chargées et donnerait des résultats incomplets dans l'environnement de développement avec le chargement automatique activé. Je ne vais pas dévaloriser, mais cela devrait peut-être être mentionné dans la réponse.
lorefnon
tarif suffisant, mise à jour
Nimir
Je suis sur Rails 6.0.2 et le eager_load! n'a pas forcé la méthode descendants à renvoyer autre chose qu'un tableau vide.
jgomo3
23

Je pense que la solution de @ hnovick est cool si vous n'avez pas de modèles sans table. Cette solution fonctionnerait également en mode développement

Mon approche est cependant subtilement différente -

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

classify est bien censé vous donner correctement le nom de la classe à partir d'une chaîne . safe_constantize garantit que vous pouvez le transformer en classe en toute sécurité sans lever d'exception. Cela est nécessaire si vous avez des tables de base de données qui ne sont pas des modèles. compact pour que tous les nils de l'énumération soient supprimés.

Aditya Sanghi
la source
3
C'est génial @Aditya Sanghi. Je n'en savais rien safe_constantize.
lightyrs
Pour les rails 2.3.x, utilisez: ActiveRecord :: Base.connection.tables.map {| x | x.classify.constantize rescue nil} .compact
iheggie
@iheggie Il est généralement préférable de publier cela comme une réponse distincte que de le modifier dans le message existant.
Pokechu22
merci, je vous ai trouvé la réponse qui me convenait le mieux #adiya
illusionniste
21

Si vous voulez juste les noms de classe:

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

Il suffit de l'exécuter dans la console Rails, rien de plus. Bonne chance!

EDIT: @ sj26 a raison, vous devez d'abord l'exécuter avant de pouvoir appeler des descendants:

Rails.application.eager_load!
Jordan Michael Rushing
la source
Juste ce que je voulais. Remercier!
sunsations
appeler mapavec puts? Je ne comprends pas le point devrait êtreActiveRecord::Base.descendants.map(&:model_name)
Nuno Costa
Vous pouvez le faire de cette façon, mais ils seront dans un seul tableau, au lieu de ligne par ligne, dans un format beaucoup plus facile à lire.
Jordan Michael Rushing
17

Cela semble fonctionner pour moi:

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

Rails ne charge les modèles que lorsqu'ils sont utilisés, donc la ligne Dir.glob "nécessite" tous les fichiers du répertoire models.

Une fois que vous avez les modèles dans un tableau, vous pouvez faire ce que vous pensiez (par exemple dans le code de vue):

<% @models.each do |v| %>
  <li><%= h v.to_s %></li>
<% end %>
bhousel
la source
Merci bhousel. À l'origine, je suis allé avec ce style d'approche, mais j'ai fini par utiliser la solution que Vincent a postée ci-dessus car cela signifiait que je n'avais pas à "modéliser" le nom du fichier (c'est-à-dire supprimer tout _, mettre en majuscule! Chaque mot, puis rejoindre à nouveau).
mr_urf
avec sous-répertoires:...'/app/models/**/*.rb'
artemave
Object.subclasses_of est obsolète après la v2.3.8.
David
11

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

vjt
la source
7
Celui-ci est agréable car, dans Rails 3, vos modèles ne sont pas chargés automatiquement par défaut, donc la plupart des méthodes ci-dessus ne renverront pas tous les modèles possibles. Ma permutation capture également des modèles dans des plugins et sous-répertoires:Dir['**/models/**/*.rb'].map {|f| File.basename(f, '.*').camelize.constantize }
wbharding
2
@wbharding C'est plutôt sympa, mais il se trompe quand il essaie de constantiser les noms de mes tests de modèle rspec. ;-)
Ajedi32
@wbharding belle solution mais ça casse quand vous avez des modèles avec espace de noms
Marcus Mansur
10

ActiveRecord::Base.connection.tables

Mark Locklear
la source
Un autre suivi intéressant est <nom_table> .noms_colonnes pour répertorier toutes les colonnes de la table. Donc, pour votre table utilisateur, vous exécuteriez User.column_names
Mark Locklear
Cela vous permettra d'obtenir toutes les tables, pas seulement les modèles, car certaines tables n'ont pas toujours de modèles associés.
courtsimas
7

En une seule ligne:

 ActiveRecord::Base.subclasses.map(&:name)
Adrian
la source
2
Cela ne montre pas tous les modèles pour moi. Pas certain de pourquoi. C'est un peu court, en fait.
courtsimas
1
travaillé pour moi. 'juste un peu tard pour répondre à tout. lui donner le temps.
boulder_ruby
2
Il est probablement nécessaire Rails.application.eager_load!avant l'exécution en mode développement.
denis.peplin
7

Je ne peux pas encore commenter, mais je pense que la réponse sj26 devrait être la meilleure réponse. Juste un indice:

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

Avec Rails 6 , Zetiwerk est devenu le chargeur de code par défaut.

Pour un chargement rapide, essayez:

Zeitwerk::Loader.eager_load_all

ensuite

ApplicationRecord.descendants
demir
la source
5

Oui, il existe de nombreuses façons de trouver tous les noms de modèles, mais ce que j'ai fait dans ma gem model_info est, cela vous donnera tous les modèles même inclus dans les gemmes.

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

puis imprimez simplement ceci

@model_array
nitanshu verma
la source
3

Cela fonctionne pour 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
la source
upvolt pour ce Rails.application.eager_load! idée
équivalent8
3

Pour éviter de précharger tous les Rails, vous pouvez procéder comme suit:

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

require_dependency (f) est le même que celui Rails.application.eager_load!utilisé. Cela devrait éviter les erreurs de fichier déjà requises.

Ensuite, vous pouvez utiliser toutes sortes de solutions pour répertorier les modèles AR, comme ActiveRecord::Base.descendants

John Owen Chili
la source
2
Module.constants.select { |c| (eval c).is_a?(Class) && (eval c) < ActiveRecord::Base }
Naveed
la source
jette TypeError: pas de conversion implicite de Symbol en String dans la console.
snowangel
1

Voici une solution qui a été approuvée avec une application Rails complexe (celle qui alimente 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

Il prend les meilleures parties des réponses dans ce fil et les combine dans la solution la plus simple et la plus approfondie. Cela gère les cas où vos modèles sont dans des sous-répertoires, utilisez set_table_name etc.

Pascal-Louis Perez
la source
1

Je viens de tomber sur celui-ci, car j'ai besoin d'imprimer tous les modèles avec leurs attributs (construits sur le commentaire 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
la source
1

Cela a fonctionné pour moi. Un merci spécial à tous les messages ci-dessus. Cela devrait renvoyer une collection de tous vos modèles.

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
la source
1

L' Railsimplémente la méthode descendants, mais les modèles n'héritent pas nécessairement de ActiveRecord::Base, par exemple, la classe qui inclut le module ActiveModel::Modelaura le même comportement qu'un modèle, mais ne sera pas liée à une table.

Complétant ainsi ce que disent les collègues ci-dessus, le moindre effort ferait cela:

Patch de singe de la classe Classdu Rubis:

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

et la méthode models , y compris les ancêtres, comme ceci:

La méthode Module.constantsrenvoie (superficiellement) une collection de symbols, au lieu de constantes, donc, la méthode Array#selectpeut être substituée comme ce patch de singe 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

Patch de singe 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

Et enfin, la méthode des modèles

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
la source
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

Cela vous donnera toutes les classes de modèles que vous avez sur votre projet.

Victor
la source
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
la source
0

J'ai essayé tellement de ces réponses sans succès dans Rails 4 (wow ils ont changé une chose ou deux pour l'amour de Dieu) J'ai décidé d'ajouter la mienne. Ceux qui ont appelé ActiveRecord :: Base.connection et tiré les noms de table ont fonctionné mais n'ont pas obtenu le résultat que je voulais parce que j'ai caché certains modèles (dans un dossier à l'intérieur de l'application / models /) que je ne voulais pas supprimer:

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

Je mets cela dans un initialiseur et je peux l'appeler de n'importe où. Empêche l'utilisation inutile de la souris.

boulder_ruby
la source
0

peut vérifier cela

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

En supposant que tous les modèles sont dans l'application / modèles et que vous avez grep & awk sur votre serveur (la majorité des cas),

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

C'est plus rapide que Rails.application.eager_load!ou en boucle dans chaque fichier avec Dir.

ÉDITER:

L'inconvénient de cette méthode est qu'elle manque des modèles qui héritent indirectement d'ActiveRecord (par exemple FictionalBook < Book). Le moyen le plus sûr est Rails.application.eager_load!; ActiveRecord::Base.descendants.map(&:name), même si c'est un peu lent.

konyak
la source
0

Je lance juste cet exemple ici si quelqu'un le trouve utile. La solution est basée sur cette réponse https://stackoverflow.com/a/10712838/473040 .

Supposons que vous ayez une colonne public_uidutilisée comme ID principal pour le monde extérieur (vous pouvez trouver les raisons pour lesquelles vous voudriez le faire ici )

Supposons maintenant que vous ayez introduit ce champ sur un tas de modèles existants et que vous vouliez maintenant régénérer tous les enregistrements qui ne sont pas encore définis. Vous pouvez faire ça comme ça

# 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

vous pouvez maintenant courir rake di:public_uids:generate

équivalent8
la source