Czy istnieje sposób, aby uzyskać kolekcję wszystkich modeli w aplikacji Rails?


201

Czy istnieje sposób, aby uzyskać kolekcję wszystkich modeli w aplikacji Rails?

Zasadniczo mogę zrobić:

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

1
Jeśli chcesz zebrać wszystkie modele, w tym modele silników / szyn Rails, zapoznaj się z odpowiedzią @jaime
Andrei

Nie działa na szynach 5.1
aks

Odpowiedzi:


98

EDYCJA: Spójrz na komentarze i inne odpowiedzi. Są mądrzejsze odpowiedzi niż ta! Lub spróbuj ulepszyć ten jako wiki społeczności.

Modele nie rejestrują się w obiekcie głównym, więc nie, Rails nie ma listy modeli.

Ale nadal możesz zajrzeć do zawartości katalogu modeli aplikacji ...

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

EDYCJA: Innym (dzikim) pomysłem byłoby użycie refleksji Ruby do wyszukiwania wszystkich klas rozszerzających ActiveRecord :: Base. Nie wiem jednak, jak wymienić wszystkie klasy ...

EDYCJA: Dla zabawy znalazłem sposób, aby wymienić wszystkie klasy

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

EDYCJA: W końcu udało się wymienić wszystkie modele bez patrzenia na katalogi

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

Jeśli chcesz również obsługiwać klasę pochodną, ​​musisz przetestować cały łańcuch nadklasy. Zrobiłem to, dodając metodę do klasy 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
Do twojej wiadomości, obliczyłem obie metody dla zabawy. Wyszukiwanie katalogów jest o rząd wielkości szybsze niż wyszukiwanie w klasach. To pewnie było oczywiste, ale teraz już wiesz :)
Edward Anderson

9
Ważne jest również, aby pamiętać, że wyszukiwanie modeli za pomocą metod stałych nie obejmie niczego, do czego nie odwoływano się od czasu uruchomienia aplikacji, ponieważ ładuje modele tylko na żądanie.
Edward Anderson

4
Wolę „Kernel.const_get stała_nazwa” niż „eval stała_nazwa”.
Jeremy Weathers

3
RAILS_ROOTnie jest już dostępny w Rails 3. Zamiast tego użyjDir.glob(Rails.root.join('app/models/*'))
fanaugen

1
W rzeczywistości modele rejestrują się jako potomkowie ActiveRecord::Baseteraz, więc jeśli chcesz załadować wszystkie modele, możesz je łatwo iterować - zobacz moją odpowiedź poniżej.
sj26,

393

Cała odpowiedź dla szyn 3, 4 i 5 brzmi:

Jeśli cache_classesjest wyłączone (domyślnie jest wyłączone w fazie rozwoju, ale włączone w produkcji):

Rails.application.eager_load!

Następnie:

ActiveRecord::Base.descendants

Dzięki temu wszystkie modele w aplikacji, niezależnie od tego, gdzie się znajdują, są załadowane, a wszystkie używane klejnoty, które udostępniają modele, są również ładowane.

Powinno to również działać na klasach, które dziedziczą ActiveRecord::Base, jak ApplicationRecordw Railsach 5, i zwracają tylko to poddrzewo potomków:

ApplicationRecord.descendants

Jeśli chcesz dowiedzieć się więcej o tym, jak to zrobić, sprawdź ActiveSupport :: DescendantsTracker .


33
Niesamowite! To powinna być zaakceptowana odpowiedź. Dla każdego z zastosowaniem tego w zadaniu natarcia: Dodaj zadanie zależą :environmentdla eager_load!do pracy.
Jo Liss,

1
Lub, jako nieco szybsza alternatywa Rails.application.eager_load!, możesz po prostu załadować modele:Dir.glob(Rails.root.join('app/models/*')).each do |x| require x end
Ajedi32,

5
@ Ajedi32, który nie jest kompletny, modele można zdefiniować poza tymi katalogami, szczególnie przy użyciu silników z modelami. Nieco lepiej, przynajmniej globalnie wszystkie Rails.paths["app/models"].existentkatalogi. Chętne ładowanie całej aplikacji jest bardziej kompletną odpowiedzią i upewni się, że absolutnie nie ma już nic do zdefiniowania modeli.
sj26

2
Rozumiem, co oznacza sj26, ale może jest mały błąd: o ile wiem w środowisku programistycznym cache_classes jest wyłączone (false), dlatego musisz ręcznie zapragnąć załadować aplikację, aby uzyskać dostęp do wszystkich modeli. wyjaśniono tutaj
masciugo

3
@ Ajedi32 ponownie, nie pełna odpowiedź. Jeśli chcesz ładować tylko modele, spróbuj:Rails.application.paths["app/models"].eager_load!
sj26 29.10.13

119

Na wypadek, gdyby ktoś natknął się na to, mam inne rozwiązanie, nie polegając na czytaniu reżimu ani rozszerzaniu klasy Class ...

ActiveRecord::Base.send :subclasses

Zwróci to tablicę klas. Więc możesz to zrobić

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

8
dlaczego nie używasz, ActiveRecord::Base.subclassesale musisz używać send? Wygląda też na to, że musisz „dotknąć” modelu, zanim się pojawi, na przykład c = Category.newi się pojawi. W przeciwnym razie nie będzie.
nonpolarity

52
W Rails 3 zmieniono to naActiveRecord::Base.descendants
Tobias Cohen

3
Musisz użyć opcji „wyślij”, ponieważ element: podklasy jest chroniony.
Kevin Rood

11
Dzięki za wskazówkę Rails 3. Dla każdego, kto przyjdzie, nadal musisz „dotknąć” modeli, zanim ActiveRecord::Base.descendantsje wypisze.
nfm

3
Technicznie w Rails 3 masz podklasy i potomków, oznaczają one różne rzeczy.
sj26

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

wróci

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

Informacje dodatkowe Jeśli chcesz wywołać metodę na nazwie obiektu bez modelu: ciąg znaków nieznana metoda lub błędy zmiennych, użyj tego

model.classify.constantize.attribute_names

8
Dzięki temu otrzymasz wszystkie tabele, nie tylko modele, ponieważ niektóre tabele nie zawsze mają powiązane modele.
courtimas

Ta odpowiedź powinna być uważana za niepoprawną, ponieważ wykonalne (i powszechne w starszych konfiguracjach) jest skonfigurowanie nazwy tabeli jako innej niż pluralizowana nazwa modelu. Ta odpowiedź daje prawidłową odpowiedź, nawet jeśli konfiguracja odbiega od konfiguracji domyślnej.
lorefnon

w niektórych przypadkach działa to lepiej niż ActiveRecord::Base.send :subclasses- szukanie nazw tabel jest dobrym pomysłem. Automatyczne generowanie nazw modeli może być problematyczne, jak wspomniano w lorefnon.
Tilo

.capitalize.singularize.camelizemożna zastąpić .classify.
Maxim

34

Szukałem sposobów na zrobienie tego i ostatecznie wybrałem ten sposób:

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

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

źródło: http://portfo.li/rails/348561-how-can-one-list-all-database-tables-from-one-project


1
Tylko w ten sposób mogę uzyskać WSZYSTKIE modele, w tym modele silników Rails używanych w aplikacji. Dzięki za wskazówkę!
Andrei

2
Kilka przydatnych metod: ActiveRecord::Base.connection.tables.each{|t| begin puts "%s: %d" % [t.humanize, t.classify.constantize.count] rescue nil end}Niektóre modele mogą nie zostać aktywowane, dlatego musisz je uratować.
Andrei

2
Dostosowanie @ Andrei jest trochę: model_classes = ActiveRecord::Base.connection.tables.collect{|t| t.classify.constantize rescue nil }.compact
Max Williams

30

Dla Rails5 modeli są teraz podklas stanowi ApplicationRecordwięc do listy wszystkich modeli w swojej aplikacji dostaniesz:

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

Lub krócej:

ApplicationRecord.descendants.collect(&:name)

Jeśli jesteś w trybie deweloperskim, będziesz musiał chętnie ładować modele przed:

Rails.application.eager_load!

1
Zakładam, że wymagałoby to, aby klasy były już załadowane i dawały niekompletne wyniki w środowisku programistycznym z włączonym automatycznym ładowaniem. Nie będę głosować negatywnie, ale być może należy o tym wspomnieć w odpowiedzi.
lorefnon

wystarczy, aktualizacja
Nimir

Jestem na Railsach 6.0.2 i eager_load! nie wykonał metody potomków, aby zwracała tylko pustą tablicę.
jgomo3

23

Myślę, że rozwiązanie @ hnovick jest fajne, jeśli nie masz modeli bez tabel. To rozwiązanie działałoby również w trybie programistycznym

Moje podejście jest jednak nieco inne -

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

klasyfikować jest dobrze podobno daje nazwę klasy z ciągiem prawidłowo . safe_constantize zapewnia, że ​​możesz bezpiecznie przekształcić go w klasę bez zgłaszania wyjątku. Jest to potrzebne, jeśli masz tabele bazy danych, które nie są modelami. kompaktować, aby wszystkie wartości zerowe w wyliczeniu zostały usunięte.


3
To niesamowite @Aditya Sanghi. Nie wiedziałam o tym safe_constantize.
lightyrs

W przypadku szyn 2.3.x użyj: ActiveRecord :: Base.connection.tables.map {| x | x.classify.constantize rescue nil} .compact
iheggie

@ iheggie Ogólnie lepiej jest opublikować to jako osobną odpowiedź niż edytować go w istniejącym poście.
Pokechu22

dzięki, znalazłem odpowiedź, która najbardziej mi odpowiada #adiya
iluzjonista

21

Jeśli chcesz tylko nazwy klas:

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

Po prostu uruchom go w konsoli Rails, nic więcej. Powodzenia!

EDYCJA: @ sj26 ma rację, musisz uruchomić to zanim zaczniesz dzwonić do potomków:

Rails.application.eager_load!

Właśnie tego chciałem. Podziękować!
sunsations

dzwonisz mapz puts? Nie rozumiem o co chodziActiveRecord::Base.descendants.map(&:model_name)
Nuno Costa

Możesz to zrobić w ten sposób, ale będą one w jednej tablicy, zamiast linii po linii, w znacznie łatwiejszym do odczytania formacie.
Jordan Michael Rushing

17

Wydaje mi się, że to działa:

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

Railsy ładują modele tylko wtedy, gdy są używane, więc linia Dir.glob „wymaga” wszystkich plików w katalogu modeli.

Po umieszczeniu modeli w tablicy możesz zrobić to, o czym myślisz (np. W widoku kodu):

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

Dzięki, Bhousel. Początkowo stosowałem ten styl, ale ostatecznie skorzystałem z rozwiązania, które Vincent opublikował powyżej, ponieważ oznaczało to, że nie musiałem również „modelować” nazwy pliku (tj. Usuwać dowolne _, wielkie litery! Każdego słowa, a następnie dołączać znowu oni).
mr_urf

z podkatalogami:...'/app/models/**/*.rb'
artemave

Object.subclasses_of jest przestarzały po wersji 2.2.8.
David J.

11

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


7
Ten jest fajny, ponieważ w Rails 3 twoje modele nie są domyślnie ładowane automatycznie, więc wiele z powyższych metod nie zwróci wszystkich możliwych modeli. Moja permutacja przechwytuje także modele we wtyczkach i podkatalogach:Dir['**/models/**/*.rb'].map {|f| File.basename(f, '.*').camelize.constantize }
wbharding

2
@ wbharding To całkiem miłe, ale popełniają błędy, gdy próbuje ustabilizować nazwy moich testów modelu rspec. ;-)
Ajedi32,

@wbharding fajne rozwiązanie, ale psuje się, gdy masz modele z przestrzenią nazw
Marcus Mansur,

10

ActiveRecord::Base.connection.tables


Miłym uzupełnieniem jest także <nazwa_tabeli> .nazwy_kolumn, aby wyświetlić wszystkie kolumny w tabeli. Więc dla swojej tabeli użytkowników wykonałbyś User.column_names
Mark Locklear

Dzięki temu otrzymasz wszystkie tabele, nie tylko modele, ponieważ niektóre tabele nie zawsze mają powiązane modele.
courtimas

7

W jednej linii:

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

2
To nie pokazuje mi wszystkich modeli. Nie pewny dlaczego. W rzeczywistości jest kilka krótkich.
courtimas

1
pracował dla mnie. „tylko trochę późno, by odpowiedzieć na to wszystko. daj temu czas.
boulder_ruby

2
Prawdopodobnie potrzebuje Rails.application.eager_load!przed wykonaniem w trybie programowania.
denis.peplin

7

Nie mogę jeszcze komentować, ale myślę, że odpowiedź sj26 powinna być najwyższą odpowiedzią. Tylko wskazówka:

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

6

Z szynami 6 , Zetiwerk stał się ładowarka domyślny kod.

Aby szybko załadować, spróbuj:

Zeitwerk::Loader.eager_load_all

Następnie

ApplicationRecord.descendants

5

Tak, istnieje wiele sposobów na znalezienie wszystkich nazw modeli, ale to, co zrobiłem w moim klejnotu model_info , daje ci wszystkie modele zawarte nawet w klejnotach.

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

następnie po prostu wydrukuj to

@model_array

3

Działa to z Railsami 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

upvolt dla tego Rails.application.eager_load! pomysł
odpowiednik8

3

Aby uniknąć wstępnego ładowania wszystkich Railsów, możesz to zrobić:

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

Wymagana_zależność (f) jest taka sama Rails.application.eager_load! używa. Powinno to uniknąć już wymaganych błędów plików.

Następnie możesz użyć wszelkiego rodzaju rozwiązań do listy modeli AR, takich jak ActiveRecord::Base.descendants


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

zgłasza TypeError: brak niejawnej konwersji Symbol na String w konsoli.
snowangel

1

Oto rozwiązanie, które zostało sprawdzone dzięki złożonej aplikacji Rails (tej, która napędza 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

Pobiera najlepsze części odpowiedzi w tym wątku i łączy je w najprostsze i najdokładniejsze rozwiązanie. To obsługuje przypadki, w których twoje modele znajdują się w podkatalogach, użyj set_table_name itp.


1

Właśnie natknąłem się na ten, ponieważ muszę wydrukować wszystkie modele z ich atrybutami (zbudowane na komentarzu @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

To zadziałało dla mnie. Specjalne podziękowania dla wszystkich powyższych postów. Powinno to zwrócić kolekcję wszystkich twoich modeli.

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

Do Railsrealizuje sposób descendants, ale modele niekoniecznie zawsze dziedziczy od ActiveRecord::Base, na przykład, klasy, która zawiera modułActiveModel::Model będzie miał takie samo zachowanie jako model, po prostu nie będzie powiązana z tabelą.

Uzupełniając to, co mówią koledzy powyżej, najmniejszy wysiłek przyniesie to:

Łatka Małpy klasy ClassRuby:

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

oraz metodę models, w tym przodków, ponieważ:

Metoda Module.constantszwraca (powierzchownie) kolekcję symbolszamiast stałych, więc metodę Array#selectmożna zastąpić tak jak ta łatka małpy 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

Łatka małpy 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

I wreszcie metoda modeli

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

To da ci wszystkie klasy modeli, które masz w swoim projekcie.


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

Próbowałem wielu z tych odpowiedzi bezskutecznie w Rails 4 (wow, zmienili jedną lub dwie rzeczy na litość boską) postanowiłem dodać własną. Te, które wywołały ActiveRecord :: Base.connection i wyciągnęły nazwy tabel, działały, ale nie uzyskały pożądanego rezultatu, ponieważ ukryłem niektóre modele (w folderze wewnątrz aplikacji / models /), których nie chciałem usunąć:

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

Umieszczam to w inicjalizatorze i mogę wywoływać to z dowolnego miejsca. Zapobiega niepotrzebnemu użyciu myszy.


0

mogę to sprawdzić

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

0

Zakładając, że wszystkie modele są w aplikacji / modelach i masz grep i awk na swoim serwerze (większość przypadków),

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

Jest to szybsze niż Rails.application.eager_load!lub zapętlanie każdego pliku za pomocą Dir.

EDYTOWAĆ:

Wadą tej metody jest brak modeli, które pośrednio dziedziczą z ActiveRecord (np FictionalBook < Book.). Najpewniejszym sposobem jest to Rails.application.eager_load!; ActiveRecord::Base.descendants.map(&:name), że jest trochę powolne.


0

Rzucam ten przykład tutaj, jeśli ktoś uzna to za przydatne. Rozwiązanie oparte jest na tej odpowiedzi https://stackoverflow.com/a/10712838/473040 .

Powiedzmy, że masz kolumnę public_uid, który jest używany jako podstawowy identyfikator światem zewnętrznym (można findjreasons dlaczego chciał to zrobić tutaj )

Powiedzmy teraz, że wprowadziłeś to pole do wielu istniejących modeli i teraz chcesz zregenerować wszystkie rekordy, które nie zostały jeszcze ustawione. Możesz to zrobić w ten sposób

# 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

możesz teraz biegać rake di:public_uids:generate

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.