Чи є спосіб отримати колекцію всіх моделей у додатку Rails?


201

Чи є спосіб отримати колекцію всіх моделей у додатку Rails?

В основному, чи можу я зробити так, як:

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

1
Якщо вам потрібно зібрати всі моделі, включаючи моделі двигунів / рейок Rails, дивіться відповідь @jaime
Андрій

Не працює на рейках 5.1
акс

Відповіді:


98

EDIT: Подивіться на коментарі та інші відповіді. Є розумніші відповіді, ніж ця! Або спробуйте покращити цю як вікі спільноти.

Моделі не реєструються в основному об'єкті, тому ні, у Rails немає списку моделей.

Але ви все ще можете шукати вміст каталогу моделей вашої програми ...

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

EDIT: Ще однією (дикою) ідеєю було б використовувати відображення Ruby для пошуку всіх класів, які розширюють ActiveRecord :: Base. Не знаю, як ви можете перелічити всі класи, хоча ...

EDIT: Просто заради задоволення я знайшов спосіб перелічити всі класи

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

EDIT: Нарешті вдалося перерахувати всі моделі, не дивлячись на каталоги

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
FYI, я присвятив обидва методи просто заради розваги. Шукати каталоги - це на порядок швидше, ніж пошук за класами. Це було, мабуть, очевидно, але тепер ви знаєте :)
Едвард Андерсон

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

393

Вся відповідь на рейки 3, 4 і 5:

Якщо cache_classesвимкнено (за замовчуванням він вимкнений у розвитку, але у виробництві):

Rails.application.eager_load!

Тоді:

ActiveRecord::Base.descendants

Це гарантує завантаження всіх моделей у вашій програмі, незалежно від того, де вони перебувають, а також будь-які дорогоцінні камені, які ви використовуєте, які надають моделі.

Це також повинно працювати над класами, які успадковують ActiveRecord::Base, як, наприклад, ApplicationRecordу Rails 5, і повертають лише те піддерево нащадків:

ApplicationRecord.descendants

Якщо ви хочете дізнатися більше про те, як це робиться, перегляньте ActiveSupport :: DescendantsTracker .


33
Дивовижно! Це має бути прийнятою відповіддю. Ні для кого , використовуючи це в задачі граблі: Зробіть ваше завдання залежить :environmentдля eager_load!до роботи.
Джо Лісс

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 вимкнено (помилково), тому вам потрібно вручну завантажувати додаток для доступу до всіх моделей. пояснено тут
masciugo

3
@ Ajedi32 знову, не повна відповідь. Якщо ви хочете завантажувати лише моделі, то спробуйте:Rails.application.paths["app/models"].eager_load!
sj26

119

На всякий випадок, коли хтось наткнеться на цей, я отримав інше рішення, не покладаючись на читання режиму чи розширення класу 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
Tobias Cohen

3
Ви повинні використовувати "відправити", оскільки член: підкласи захищений.
Кевін Род

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.classify.constantize.attribute_names

8
Це дасть вам усі таблиці, але не лише моделі, оскільки деякі таблиці не завжди мають асоційовані моделі.
Courtimas

Цю відповідь слід вважати помилковою, оскільки це можливо (і поширене в застарілих налаштуваннях), щоб налаштувати ім’я таблиці таким, що не відрізняється від плюралізованого імені моделі. Ця відповідь дає правильну відповідь, навіть коли налаштування відхиляється від конфігурації за замовчуванням.
lorefnon

в деяких випадках це працює краще, ніж ActiveRecord::Base.send :subclasses- хороша ідея пошуку назв таблиць. Автоматичне генерування імен моделей може бути проблематичним, як згадується lorefnon.
Тіло

.capitalize.singularize.camelizeможна замінити на .classify.
Максим

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}Деякі моделі можуть бути не активовані, тому вам потрібно її врятувати.
Андрій

2
Адаптація @ Андрея трохи: model_classes = ActiveRecord::Base.connection.tables.collect{|t| t.classify.constantize rescue nil }.compact
Макс Вільямс

30

Для Rails5 моделей тепер підкласи з ApplicationRecordтак , щоб отримати список всіх моделей в вашому додатку ви робите:

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

Або коротше:

ApplicationRecord.descendants.collect(&:name)

Якщо ви перебуваєте в режимі розробки, вам доведеться нетерпляче завантажувати моделі раніше:

Rails.application.eager_load!

1
Я вважаю, що для цього потрібно, щоб класи вже завантажувались і давали б неповні результати в середовищі розробки з включеною автоматичною завантаженням. Я не заперечую, але, можливо, це слід сказати у відповіді.
lorefnon

вартість проїзду, оновлення
Німір

Я на Rails 6.0.2 і на eager_load! не зробив метод нащадків повернути нічого, крім порожнього масиву.
jgomo3

23

Я думаю, що рішення @ hnovick - це круте рішення, якщо у вас немає моделей без столу. Це рішення також буде працювати в режимі розробки

Мій підхід дещо інший -

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

класифікація належним чином дає вам ім'я класу з рядка належним чином . safe_constantize гарантує, що ви можете безпечно перетворити його в клас, не кидаючи винятку. Це потрібно, якщо у вас є таблиці баз даних, які не є моделями. компактний, так що будь-які нулі в перерахунку видаляються.


3
Це приголомшливо @Aditya Sanghi. Я про це не знав safe_constantize.
lightyrs

Для рейок 2.3.x використовуйте: ActiveRecord :: Base.connection.tables.map {| x | x.classify.constantize рятувальний nil} .compact
iheggie

@iheggie Це, як правило, краще розмістити як окрему відповідь, ніж редагувати її в існуючій публікації.
Pokechu22

дякую, я знайшов, що ти найкраще підходить для мене #adiya
ілюзіоніст

21

Якщо ви хочете лише назви класу:

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

Просто запустіть його в консолі Rails, нічого більше. Удачі!

EDIT: @ sj26 правильно, вам потрібно запустити це першим, перш ніж ви зможете викликати нащадків:

Rails.application.eager_load!

Просто те, що я хотів. Спасибі!
сонця

виклик mapз puts? Я не розумію, що справа повинна бутиActiveRecord::Base.descendants.map(&:model_name)
Нуно Коста,

Ви можете це зробити так, але вони будуть знаходитися в одному масиві замість рядка за рядком у набагато простішому для читання форматі.
Джордан Майкл Рашінг

17

Це, здається, працює для мене:

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

Rails завантажує моделі лише тоді, коли вони використовуються, тому рядок Dir.glob "вимагає" всіх файлів у каталозі моделей.

Як тільки у вас є моделі в масиві, ви можете робити те, про що думали (наприклад, у коді перегляду):

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

Спасибі байхаку. Я спочатку пішов з таким стилем підходу, але в кінцевому підсумку використовував рішення, яке розмістив Вінсент вище, оскільки це означало, що мені також не потрібно "моделювати" ім'я файлу (тобто викреслювати будь-яке _, з великої літери! Кожне слово, а потім приєднуватися їх знову).
mr_urf

з підкаталогами:...'/app/models/**/*.rb'
artemave

Об'єкт.subclasses_of застарілий після v2.3.8.
Девід Дж.

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

2
@wbharding Це дуже приємно, але він помиляється, коли намагається узгодити назви моїх тестів на модель rspec. ;-)
Ajedi32

@wbharding приємне рішення, але воно порушується, коли у вас є моделі з простором імен
Marcus Mansur

10

ActiveRecord::Base.connection.tables


Також приємним поданням є <table_name> .column_names, щоб перелічити всі стовпці таблиці. Отже, для вашої таблиці користувачів ви будете виконувати User.column_names
Марк

Це дасть вам усі таблиці, але не лише моделі, оскільки деякі таблиці не завжди мають асоційовані моделі.
Courtimas

7

Лише в одному рядку:

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

2
Це показує не всі моделі для мене. Не знаю чому. Це насправді пара коротко.
Courtimas

1
працював на мене. 'лише трохи пізно, щоб відповісти на це все. приділіть час.
boulder_ruby

2
Можливо, це потрібно Rails.application.eager_load!перед виконанням в режимі розробки.
denis.peplin

7

Я поки не можу коментувати, але вважаю, що відповідь sj26 має бути головною. Просто натяк:

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

6

З рейками 6 , Зетиверк став код за замовчуванням Завантажувач.

Для швидкого завантаження спробуйте:

Zeitwerk::Loader.eager_load_all

Тоді

ApplicationRecord.descendants

5

Так, є багато способів знайти всі назви моделей, але те, що я зробив у своєму самоцвіті model_info , це дасть вам всі моделі, навіть включені до дорогоцінних каменів.

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! ідея
еквівалент8

3

Щоб уникнути попереднього завантаження всіх рейок, ви можете зробити це:

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

need_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

1

Ось рішення, яке було перевірено складним додатком Rails (той, що живить площу)

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 буде мати таку ж поведінку , як модель, просто не буде пов'язаний з таблицею.

Отже, доповнюючи те, що говорять колеги вище, найменші зусилля зробили б це:

Мавпа Патч класу ClassРубі:

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 і витягували назви таблиць, працювали, але не отримали результату, який я хотів, тому що я заховав деякі моделі (у папці всередині додатка / моделей /), які не хотів видалити:

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

Якщо припустити, що всі моделі є в додатках / моделях, і у вас на сервері є греп & 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який використовується як основний ідентифікатор для зовнішнього світу (ви можете знайтипричини, чому ви хочете це зробити тут )

Тепер скажімо, що ви ввели це поле на купу існуючих моделей і тепер ви хочете відновити всі записи, які ще не встановлені. Ви можете зробити це так

# 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.