Это руководство документирует, как работает автозагрузка и перезагрузка констант в режиме classic
.
После его прочтения, вы узнаете:
autoload_paths
и как работает нетерпеливая загрузка в production
require_dependency
Это руководство документирует автоматическую загрузку в режиме classic
, являющимся традиционным. Если, вместо этого, вы хотите прочитать о режиме zeitwerk
, новинкой Rails 6, смотрите Автозагрузка и перезагрузка констант (режим Zeitwerk).
Ruby on Rails позволяет приложениям быть написанными так, как будто их код был предварительно загружен.
В обычных классах в программах на Ruby необходимо загружать их зависимости:
require "application_controller"
require "post"
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
Наши рубишные инстинкты быстро усматривают некоторую избыточность тут: если классы были объявлены в файлах, соответствующих их имени, нельзя ли как-то автоматизировать их загрузку? Можно было бы избавиться от просмотра файла на предмет сломавшихся зависимостей.
Более того, Kernel#require
загружает файлы единожды, но разработка становится гораздо приятнее, когда для перезагрузки кода не нужно перезагружать сервер. Было бы неплохо иметь возможность использования Kernel#load
в development и Kernel#require
в production.
Разумеется, эти особенности представлены Ruby on Rails, в котором мы просто пишем
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
Настоящее руководство раскрывает, как это работает.
Хотя константы просты в большинстве языков программирования, они являются большой темой в Ruby.
Документирование констант Ruby находится за пределами этого руководства, но тем не менее мы собираемся осветить несколько тем. Понимание следующих разделов играет важную роль в понимании автозагрузки и перезагрузки констант.
Определения класса и модуля могут быть вложены, чтобы создать пространство имен:
module XML
class SAXParser
# (1)
end
end
Вложенность в любое заданное место — это коллекция из внешних вложенных объектов класса и модуля для доступа снаружи. Вложенность в любом заданном месте можно просмотреть с помощью Module.nesting
. Например, в предыдущем примере вложенностью (1) является
[XML::SAXParser, XML]
Важно понимать, что вложенность состоит из объектов класса и модуля, она не делает ничего с константами, использованными для доступа к ним, и никак не относится к их именам.
Например, хотя это определение похоже на предыдущее:
class XML::SAXParser
# (2)
end
вложенность (2) отличается:
[XML::SAXParser]
XML
не принадлежит ей.
Из этого примера мы видим, что имя класса или модуля, принадлежащего определенной вложенности, не обязательно соотносится с пространством имен в этом месте.
Более того, они абсолютно независимы, для примера
module X
module Y
end
end
module A
module B
end
end
module X::Y
module A::B
# (3)
end
end
Вложенность (3) состоит из двух объектов модуля:
[A::B, X::Y]
Таким образом, она не только не заканчивается на A
, который даже не принадлежит вложенности, она также содержит X::Y
, который независим от A::B
.
Вложенность — это внутренний стек, поддерживаемый интерпретатором, и он модифицируется в соответствии со следующими правилами:
Объект класса, следующий за ключевым словом class
добавляется, когда выполняется его тело, а затем извлекается.
Объект класса, следующий за ключевым словом module
добавляется, когда выполняется его тело, а затем извлекается.
Синглтон-класс, открытый с помощью class << object
добавляется, затем извлекается.
Когда вызывается instance_eval
со строковым аргументом, синглтон-класс получателя добавляется во вложенность вычисляемого кода. Когда вызываются class_eval
или module_eval
со строковым аргументом, получатель добавляется во вложенность вычисляемого кода.
Вложенность в коде верхнего уровня, интерпретируемого Kernel#load
, пустая, за исключением случая, когда вызов load
получает true в качестве второго аргумента, в случае чего Ruby добавляет вновь созданный анонимный модуль.
Любопытно, что блоки не модифицируют стек. В частности, блоки, переданные в Class.new
и Module.new
, не добавляют определяемые класс или модуль в их вложенность. Это одно из отличий между определением классов или модулей тем или иным способом.
Допустим, следующий код создает класс (а не переоткрывает его):
class C
end
Ruby создает константу C
в Object
и сохраняет в эту константу объект класса. Именем экземпляра класса является "C", строка, названная по имени константы.
То есть,
class Project < ApplicationRecord
end
выполняет назначение константе, эквивалентное
Project = Class.new(ApplicationRecord)
включая установление имени класса в качестве побочного эффекта:
Project.name # => "Project"
У назначения констант есть специальное правило, благодаря которому это происходит: если назначаемый объект является анонимным классом или модулем, Ruby устанавливает имя объекта по константе.
С этого момента то, что будет происходить с константой или экземпляром, не имеет значения. Например, константа может быть удалена, объект класса может быть назначен другой константе, больше не храниться в константе, и так далее. Как только имя установлено, оно не меняется.
Аналогично, создание модуля с помощью ключевого слова module
, как в
module Admin
end
выполняет назначение константы, эквивалентное
Admin = Module.new
включая установление имени в качестве побочного эффекта:
Admin.name # => "Admin"
Контекст выполнения блока, переданного в Class.new
или Module.new
, не полностью эквивалентен контексту тела определений с помощью ключевых слов class
и module
. Но обе идиомы приводят к одинаковому назначению константы.
Таким образом, когда кто-то говорит "класс String
", технически это означает объект класса, хранимого в константе с именем "String". Эта константа, в свою очередь, принадлежит объекту класса, хранящемуся в константе, называемой "Object".
String
— это обычная константа, и к ней применяется все, относящееся к константам, например, применяемые к ней алгоритмы резолюции.
Более того, в контроллере
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
Post
— это не синтаксис для класса. Скорее, Post
— это обычная константа Ruby. Если все нормально, константа вычисляет объект, отвечающий на all
.
Вот почему мы говорим об автозагрузке констант, в Rails есть возможность загрузки констант на лету.
Константы принадлежат модулям в буквальном смысле. У классов и модулей есть таблица констант; рассматривайте ее как хэш-таблицу.
Давайте проанализируем пример, чтобы в действительности понять, что это означает. Хотя распространенные упрощения языка, наподобие "класс String
", удобны, в обучающих целях изложение здесь будет более точным.
Рассмотрим следующее определение модуля:
module Colors
RED = '0xff0000'
end
Во-первых, когда обрабатывается ключевое слово module
, интерпретатор создает новую запись в таблице констант объекта класса, хранимого в константе Object
. Упомянутая запись связывает имя "Colors" с вновь созданным объектом модуля. Далее интерпретатор устанавливает имя нового объекта модуля равным строке "Colors".
Далее, когда интерпретируется тело определения модуля, создается запись в таблице констант объекта модуля, хранящегося в константе Colors
. Эта запись связывает имя "RED" со строкой "0xff0000".
В частности, Colors::RED
никоим образом не относится к любой другой константе RED
, которая может находиться в любом другом объекте класса или модуля. Если такие существуют, они будут иметь другие записи в их соответствующих таблицах констант.
Обратите особенное внимание в предыдущих абзацах на различие между объектами класса и модуля, именами констант и объектами значений, связанными с ними в таблицах констант.
Давайте определим, что в любом месте кода, cref будет обозначать первый элемент вложенности, если она не пустая, а в противном случае Object
.
Не вдаваясь глубоко в детали, алгоритм резолюции для ссылок на относительные константы выполняется так:
Если вложенность не пустая, константа ищется в ее элементах по порядку. Предки этих элементов игнорируются.
Если не найдена, алгоритм проходит по цепочке предков у cref.
Если не найдена и cref — это модуль, константа ищется в Object
.
Если не найдена, вызывается const_missing
на cref. Реализация по умолчанию для const_missing
вызывает NameError
, но может быть переопределена.
Автозагрузка Rails не эмулирует этот алгоритм, но его отправной точкой является имя константы, которую нужно автоматически загрузить, и cref. Подробнее в главе Относительные ссылки.
Ограниченные константы выглядят так:
Billing::Invoice
Billing::Invoice
состоит из двух констант: Billing
является относительной, и она разрешается с помощью алгоритма из предыдущего раздела.
Начинание с двоеточий делает первый сегмент абсолютным, а не относительным: ::Billing::Invoice
. Это принуждает искать Billing
среди констант верхнего уровня.
Invoice
, с другой стороны, ограничена Billing
, и далее нам нужно разрешить ее. Давайте определим, что parent будет обозначать этот ограничивающий объект класса или модуля, то есть Billing
в вышеприведенном примере. Алгоритм для ограниченных констант выполняется так:
Константа ищется в parent и его предках. В Ruby >= 2.5, Object
пропускается, если присутствует среди предков. Kernel
и BasicObject
все еще проверяются.
Если поиск неудачный, в parent вызывается const_missing
. Реализация по умолчанию для const_missing
вызывает NameError
, но может быть переопределена.
В Ruby < 2.5 String::Hash
вычисляется как Hash
и интерпретатор выдает предупреждение: "toplevel constant Hash referenced by String::Hash". Начиная с 2.5, String::Hash
вызывает NameError
, потому что Object
пропускается.
Как видите, этот алгоритм проще, чем для относительных констант. В частности, вложенность не имеет значения, и нет специального случая для модулей, когда у них или их предков нет константы, Object
не проверяется.
Автозагрузка Rails не эмулирует этот алгоритм, но его отправной точкой является имя константы, которую нужно автоматически загрузить, и parent. Подробнее в главе Ограниченные ссылки.
Для заданной строки с константой мы определяем родительское пространство имен, как строку, полученную в результате удаления самого правого сегмента.
Например, родительским пространством имен для строки "A::B::C" является строка "A::B", родительским пространством имен для "A::B" является "A", и родительским пространством имен для "A" является "".
Впрочем, интерпретация родительского пространства имен может быть запутанной, если размышлять о классах и модулях. Давайте рассмотрим модуль M с именем "A::B":
Родительское пространство имен, "A", может не отражать вложенность в заданной точке.
Константа A
может больше не существовать, некий код мог удалить ее из Object
.
Если A
существует, класс или модуль, который изначально был в A
, может больше не быть там. Например, если после того, как константу удалили, произошло новое назначение константы, положившее иной объект в нее.
В этом случае может произойти так, что переназначенная A
содержит новый класс или модуль, также названный "A"!
В прошлых сценариях M больше не будет доступен с помощью A::B
. но сам объект модуля будет где-то существовать, и его имя по прежнему будет "A::B".
Идея родительского пространства имен является сердцевиной алгоритмов автозагрузки и помогает объяснить и интуитивно понять их мотивацию, но, как вы увидели, эта метафора легко ускользает. С учетом крайних случаев, о которых мы только что говорили, всегда принимайте во внимание то, что под "родительским пространством имен" руководство понимает исключительно определенное образование строк.
Rails автоматически загружает файлы с помощью Kernel#load
, когда config.cache_classes
является false, по умолчанию в среде development, а в противном случае с помощью Kernel#require
, по умолчанию в среде production.
Kernel#load
позволяет Rails выполнять файлы более, чем однажды, если включена перезагрузка констант.
Это руководство часто использует слово "загрузка", что означает, что интерпретируется данный файл, но фактическим механизмом может быть Kernel#load
или Kernel#require
, в зависимости от этого флажка.
Rails всегда способен автоматически загружать свое окружение на месте. Например, команда runner
делает автозагрузку:
$ bin/rails runner 'p User.column_names'
["id", "email", "created_at", "updated_at"]
Консоль, тестовый набор и, конечно же, приложение делают автозагрузку.
По умолчанию Rails нетерпеливо загружает файлы приложения, когда он запускается в режиме production, поэтому большая часть автозагрузки, происходящая в development, не происходит. Но автозагрузка все еще может произойти в течение нетерпеливой загрузки.
Например, дан
class BeachHouse < House
end
если House
все еще неизвестен, когда нетерпеливо загружается app/models/beach_house.rb
, Rails его загрузит автоматически.
Как вам, наверное, известно, когда require
получает относительное имя файла:
require "erb"
Ruby ищет файл в директориях, перечисленных в $LOAD_PATH
. То есть, Ruby перебирает все свои директории и для каждой из них проверяет, имеется ли в ней файл с именем "erb.rb", или "erb.so", или "erb.o", или "erb.dll". Если он находит один из них, то интерпретатор загружает его и заканчивает поиск. В противном случае он снова пытается сделать то же самое для следующей директории в списке. Если список заканчивается, вызывается LoadError
.
Мы попытаемся подробнее раскрыть, как работает автозагрузка констант, позже, но идея в том, что когда вызвана, но отсутствует константа, наподобие Post
, и, к примеру, имеется файл post.rb
в app/models
, Rails собирается найти его, вычислить его и получить определение Post
в качестве побочного эффекта.
Отлично, в Rails есть коллекция директорий, подобная $LOAD_PATH
, для поиска post.rb
. Эта коллекция называется autoload_paths
, и, по умолчанию, она содержит:
Все поддиректории app
в приложении и engine-ах, существующие на момент загрузки. Например, app/controllers
. Нет каких-либо значений по умолчанию, любая произвольная директория, наподобие app/workers
, будет автоматически принадлежать autoload_paths
.
Любые существующие директории второго уровня с именем app/*/concerns
в приложении и engine-ах.
Директория test/mailers/previews
.
Первоначально eager_load_paths
- это пути к вышеприведенному app
.
То, как файлы автоматически загружаются зависит от настроек конфигурации eager_load
и cache_classes
, которые обычно различаются в режимах development, production и test:
eager_load
должен быть установлен в false
, и Rails будет автозагружать файлы по мере необходимости, (смотрите Алгоритмы автозагрузки ниже) -- и затем перезагружать их, когда они изменяются (смотрите Перезагрузка констант ниже).
eager_load
устанавливается в true
, и затем во время загрузки (до того, как приложение будет готово к приему запросов) Rails загружает все файлы в eager_load_paths
, а затем отключает автоматическую загрузку (стоит отметить, что автозагрузка может потребоваться во время нетерпеливой загрузки). Работа без автозагрузки после загрузки является хорошей практикой
, поскольку автозагрузка может привести к тому, что приложение будет иметь проблемы с тредобезопасностью.
eager_load
устанавливается в false
, по этой же причине Rails имеет такое же поведением и в development.
То, что описано выше, является значениями по умолчанию для вновь сгенерированных приложений Rails. Существует несколько способов как это может быть настроено (подробнее читайте в руководстве Конфигурирование приложений на Rails). В прошлом, до Rails 5, разработчики могли настраивать autoload_paths
, добавляя дополнительное место расположения (например, lib
, которое раньше было списком путей автозагрузки, но больше таким не является). Однако теперь это не рекомендуется в большинстве случаев, поскольку, вероятно, приведет к ошибкам, связанным с production средой. Можно добавить новые места расположения в config.eager_load_paths
и config.autoload_paths
, но тогда придется использовать это на свой страх и риск.
Смотрите также раздел Автозагрузка в среде test.
Значение autoload_paths
можно просмотреть. В только что сгенерированном приложении она (отредактировано):
$ bin/rails runner 'puts ActiveSupport::Dependencies.autoload_paths'
.../app/assets
.../app/channels
.../app/controllers
.../app/controllers/concerns
.../app/helpers
.../app/jobs
.../app/mailers
.../app/models
.../app/models/concerns
.../activestorage/app/assets
.../activestorage/app/controllers
.../activestorage/app/javascript
.../activestorage/app/jobs
.../activestorage/app/models
.../actioncable/app/assets
.../actionview/app/assets
.../test/mailers/previews
autoload_paths
вычисляется и кэшируется на этапе процесса инициализации. Необходимо перезапустить приложение, чтобы отразить изменения в структуре директорий.
Это доступно только для Ruby < 2.5.
Относительная константная ссылка может появиться в нескольких местах, например, в
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
все три константные ссылки относительны.
class
и module
Ruby выполняет поиск констант, следующих за ключевыми словами class
или module
, так как ему необходимо знать, собирается ли класс или модуль быть созданным или переоткрытым.
Если константа в этом месте не определена, она не рассматривается отсутствующей константой, автозагрузка не срабатывает.
Таким образом, в предыдущем примере, если PostsController
не определен на момент интерпретации файла, автозагрузка Rails не сработает, Ruby всего лишь определит контроллер.
С другой стороны, если ApplicationController
неизвестен, константа рассматривается отсутствующей, и Rails собирается предпринять автозагрузку.
Чтобы загрузить ApplicationController
, Rails проходит по autoload_paths
. Сначала он проверяет, существует ли app/assets/application_controller.rb
. Если нет, что неудивительно, он продолжает и находит app/controllers/application_controller.rb
.
Если файл определяет константу ApplicationController
, то все хорошо, в противном случае вызывается LoadError
:
unable to autoload constant ApplicationController, expected
<full path to application_controller.rb> to define it (LoadError)
Rails не требует, чтобы значения автозагружаемых констант были объектом класса или модуля. Например, если файл app/models/max_clients.rb
определяет MAX_CLIENTS = 100
, автозагрузка MAX_CLIENTS
также сработает.
Автозагрузка ApplicationController
ищет непосредственно в директориях autoload_paths
, так как вложенность в этом месте пустая. Ситуация с Post
иная, вложенность в этой строчке является [PostsController]
, и в игру вступает поддержка пространств имен.
Основная идея в том, что для
module Admin
class BaseController < ApplicationController
@@all_roles = Role.all
end
end
чтобы автоматически загрузить Role
, мы собираемся по очереди проверить, определен ли он в текущем или родительском пространстве имен. Таким образом, концептуально мы хотим попытаться автоматически загрузить любой из
Admin::BaseController::Role
Admin::Role
Role
в этом порядке. Такова идея. Для этого Rails ищет в autoload_paths
следующие имена файлов в указанном порядке:
admin/base_controller/role.rb
admin/role.rb
role.rb
производя дополнительные поиски модулей в директориях, о которых мы скоро расскажем.
'Constant::Name'.underscore
дает относительный путь без расширения для имени файла, в котором ожидается определение для Constant::Name
.
Давайте посмотрим, как Rails автоматически загружает константу Post
в вышеприведенном PostsController
, предполагая, что модель Post
определена в
app/models/post.rb
.
Сначала он проверяет posts_controller/post.rb
в autoload_paths
:
app/assets/posts_controller/post.rb
app/controllers/posts_controller/post.rb
app/helpers/posts_controller/post.rb
...
test/mailers/previews/posts_controller/post.rb
Так как поиск завершился неуспешно, выполняется схожий поиск для директорий, почему — мы узнаем в следующем разделе:
app/assets/posts_controller/post
app/controllers/posts_controller/post
app/helpers/posts_controller/post
...
test/mailers/previews/posts_controller/post
Если все эти попытки провалятся, то Rails снова начнет поиск в родительском пространстве имен. В этом случае остается только верхний уровень:
app/assets/post.rb
app/controllers/post.rb
app/helpers/post.rb
app/mailers/post.rb
app/models/post.rb
Соответствующий файл найдется в app/models/post.rb
. Тут поиск остановится и файл загрузится. Если файл в действительности определяет Post
, все в порядке, в противном случае будет вызвано LoadError
.
Когда отсутствует ограниченная константа, Rails не ищет ее в родительских пространствах имен. Но есть нюанс: когда отсутствует константа, Rails неспособен определить, была ли она относительной или ограниченной.
Например, рассмотрим
module Admin
User
end
и
Admin::User
Если отсутствует User
, Rails в любом случае знает, что константа с именем "User" отсутствует в модуле с именем "Admin".
Если на верхнем уровне имеется User
, Ruby вернет его для первого примера, но не для второго. В целом, Rails не эмулирует алгоритмы разрешения констант Ruby, но в этом случае он пытается, используя следующую эвристику:
Если ни в одном из родительских пространств имен класса или модуля нет отсутствующей константы, тогда Rails полагает, что ссылка относительная. В противном случае ограниченная.
Например, если этот код запускает автозагрузку
Admin::User
и константа User
уже присутствует в Object
, тогда невозможна такая ситуация
module Admin
User
end
так как в этом случае Ruby вычислил бы User
, и автозагрузка в первом случае не была бы запущена. Поэтому Rails полагает ограниченную ссылку и рассматривает файл admin/user.rb
и директорию admin/user
в качестве единственных валидных вариантов.
На практике это работает достаточно хорошо, пока вложенность соответствует всем родительским пространствам имен, и константы, позволяющие применить правило, известны к этому времени.
Однако, автозагрузка происходит по требованию. Есть шанс, что User
верхнего уровня еще не был загружен, тогда Rails предполагает по соглашению относительную ссылку.
Конфликты имен подобного рода редки на практике, но если происходят, решением является require_dependency
, чтобы убедиться, что константа, необходимая для запуска эвристики, определена в конфликтующем месте.
Когда модуль выступает в роли пространства имен, Rails не требует, чтобы приложение определяло для него файл, достаточно соответствующей директории.
Допустим, у приложения есть админка, контроллеры которой хранятся в app/controllers/admin
. Если модуль Admin
пока не загружен на момент обращения к Admin::UsersController
, Rails необходимо сперва автоматически загрузить константу Admin
.
Если в autoload_paths
имеется файл с именем admin.rb
, Rails загрузило бы его, но если такого файла нет, а найдена директория с именем admin
, Rails на лету создаст пустой модуль и назначит его константе Admin
.
Относительные ссылки считаются отсутствующими в cref, где они вызваны, а ограниченные ссылки считаются отсутствующими в их parent (определение cref смотрите в Алгоритмы резолюции для относительных констант в начале этого руководства, а определение parent — в Алгоритмы резолюции для ограниченных констант)
Процедура автозагрузки константы C
в произвольной ситуации следующая:
if (класс или модуль, в котором отсутствует C, это Object)
ns = ''
else
M = (класс или модуль, в котором отсутствует C)
if (M анонимный)
ns = ''
else
ns = M.name
end
end
loop do
# Ищем обычный файл.
for dir in autoload_paths
if (существует файл "#{dir}/#{ns.underscore}/c.rb")
load/require "#{dir}/#{ns.underscore}/c.rb"
if (теперь определена C)
return
else
raise LoadError
end
end
end
# Ищем автоматический модуль.
for dir in autoload_paths
if (существует директория "#{dir}/#{ns.underscore}/c")
if (ns пустая строка)
C = (Module.new в Object) and return
else
C = (Module.new в ns.constantize) and return
end
end
end
if (ns пустое)
# Мы достигли верхнего уровня и не нашли константу.
raise NameError
else
if (C существует в любом из родительских пространств имен)
# Эвристика для ограниченных констант.
raise NameError
else
# Снова пытаемся в родительском пространстве имен.
ns = (родительское пространство имен для ns) and retry
end
end
end
Автозагрузка констант запускается по требованию, а, следовательно, код, использующий определенную константу, может иметь ее уже определенной или может запустить автозагрузку. Это зависит от последовательности выполнения кода и может различаться между запусками.
Однако бывает, когда вы хотите убедиться, что определенная константа известна к моменту, когда выполнение достигнет некоторого кода. require_dependency
представляет способ загрузки файла с помощью текущего механизма загрузки и отслеживания констант, определенном в этом файле так, как будто они были автоматически загружены, чтобы при надобности перезагрузить их.
require_dependency
редко необходим, но имеется ряд сценариев, таких как Автозагрузка и STI и Когда константы не находятся.
В отличие от автозагрузки, require_dependency
не ожидает, что файл определяет какую-либо определенную константу. Чтобы использование этой особенности не являлось плохой практикой, файлы и константы должны соответствовать друг другу.
Когда config.cache_classes
равно false, Rails способен перезагружать автозагруженные константы.
Например, если вы в консоли и отредактировали какой-то файл, код может быть перезагружен с помощью команды reload!
:
irb> reload!
Когда запущено приложение, код перезагружается когда изменяется что-то, относящееся к его логике. Для этого Rails наблюдает за рядом вещей:
config/routes.rb
.
Локали.
Файлы Ruby в autoload_paths
.
db/schema.rb
и db/structure.sql
.
Если что-то из них изменяется, имеется промежуточная программа, определяющая это и перезагружающая код.
Автозагрузка отслеживает автозагруженные константы. Перезагрузка реализована как удаление их из соответствующих классов и модулей с помощью Module#remove_const
. Таким образом, когда начнет исполняться код, эти константы снова становятся неизвестными, и файлы загружаются по требованию.
Это операция "все или ничего", Rails не пытается перезагрузить только то, что изменилось, так как зависимости между классами могут быть очень изощренными. Вместо этого все удаляется.
Module#autoload
представляет ленивый способ загрузки констант, который полностью интегрирован с алгоритмами поиска констант в Ruby, API динамических констант, и так далее. Он весьма прозрачный.
Rails широко используют его внутренне, чтобы отложить столько работы, сколько возможно в процессе загрузки. Но автозагрузка констант в Rails не реализуется с помощью Module#autoload
.
Одной из возможных реализаций, основанных на Module#autoload
, могла бы быть прохождение по дереву приложения и вызов autoload
, связывающий имена существующих файлов с именами констант по соглашению.
Имеется ряд причин, предотвращающих от использования этой реализации в Rails.
Например, Module#autoload
способен только загружать файлы с помощью require
, поэтому перезагрузка невозможна. Кроме того, он использует собственный require
, который не Kernel#require
.
Далее, он не представляет способ убрать определение в случае удаления файла. Если константа была удалена с помощью Module#remove_const
, ее autoload
снова не включится. А также он не поддерживает ограниченные имена, поэтому файлы в пространстве имен должны интерпретироваться в процессе прохождения по дереву, чтобы установить собственные вызовы autoload
, но эти файлы могут иметь собственные константные ссылки, которые еще не настроены.
Реализация с помощью Module#autoload
могла бы быть великолепной, но, как видите, как минимум сегодня она не возможна. Автозагрузка констант в Rails реализована с помощью Module#const_missing
, и поэтому у нее есть свои соглашения, документированные в этом руководстве.
Давайте рассмотрим
module Admin
class UsersController < ApplicationController
def index
@users = User.all
end
end
end
и
class Admin::UsersController < ApplicationController
def index
@users = User.all
end
end
Чтобы разрешить User
, Ruby проверяет Admin
в первом, но не в последнем случае, так как он не принадлежит вложенности (подробнее во Вложенность и Алгоритмы резолюции)
К сожалению, автозагрузка Rails не знает о вложенности в месте, где отсутствует константа, и не способен сработать так, как Ruby. В частности, Admin::User
автоматически загрузится в любом случае.
Хотя ограниченные константы с ключевыми словами class
и module
технически могут работать с автозагрузкой в некоторых случаях, вместо них предпочтительней использовать относительные константы:
module Admin
class UsersController < ApplicationController
def index
@users = User.all
end
end
end
Давайте рассмотрим:
# app/models/blog.rb
module Blog
def self.table_name_prefix
"blog_"
end
end
# app/models/blog/post.rb
module Blog
class Post < ApplicationRecord
end
end
Имя таблицы для Blog::Post
должно быть blog_posts
благодаря существованию метода Blog.table_name_prefix
. Однако, если app/models/blog/post.rb
запускается до app/models/blog.rb
, Active Record не в курсе о существовании такого метода, и предполагает, что таблица posts
.
Чтобы разрешить подобную ситуацию, нужно ясно представлять, какой файл определяет модуль Blog
(app/models/blog.rb
), а какой переоткрывает его (app/models/blog/post.rb
). Затем нужно убедиться, что определение запускается раньше, с помощью require_dependency
:
# app/models/blog/post.rb
require_dependency "blog"
module Blog
class Post < ApplicationRecord
end
end
Наследование с единой таблицей (STI) — это особенность Active Record, позволяющая хранить иерархию моделей в одной отдельной таблице. API таких моделей знает об иерархии и инкапсулирует некоторые общие потребности. Например, имеем следующие классы:
# app/models/polygon.rb
class Polygon < ApplicationRecord
end
# app/models/triangle.rb
class Triangle < Polygon
end
# app/models/rectangle.rb
class Rectangle < Polygon
end
Triangle.create
создает запись, представляющую треугольник, и Rectangle.create
создает запись, представляющую прямоугольник. Если id
— это идентификатор существующей записи, Polygon.find(id)
возвратит объект правильного типа.
Методы, оперирующие с коллекциями, также в курсе об иерархии. Например, Polygon.all
возвратит все записи из таблицы, так как все прямоугольники и треугольники — многоугольники. Active Record берет на себя заботу о возврате экземпляров соответствующего класса в результирующей выборке.
Типы автоматически загружаются при необходимости. Например, если Polygon.first
— это прямоугольник, и Rectangle
не был еще загружен, Active Record автоматически загрузит его, и запись инициализируется правильно.
Все хорошо, но если вместо выполнения запросов, основанных на корневом классе, нам необходимо работать на некотором подклассе, становится интереснее.
Хотя, работая с Polygon
, вам не нужно беспокоится обо всех его потомках, так как все в таблице многоугольники по определению, но при работе с подклассами Active Record необходимо перечислить типы, которые он ищет. Давайте рассмотрим пример.
Rectangle.all
загружает только прямоугольники, добавляя ограничение типа в запрос:
SELECT "polygons".* FROM "polygons"
WHERE "polygons"."type" IN ("Rectangle")
Теперь давайте представим подкласс Rectangle
:
# app/models/square.rb
class Square < Rectangle
end
Теперь Rectangle.all
должен вернуть прямоугольники и квадраты:
SELECT "polygons".* FROM "polygons"
WHERE "polygons"."type" IN ("Rectangle", "Square")
Но тут есть нюанс: как Active Record узнает, что класс Square
вообще существует?
Даже если существует файл app/models/square.rb
, определяющий класс Square
, если код еще не использовал этот класс, Rectangle.all
выполнит запрос
SELECT "polygons".* FROM "polygons"
WHERE "polygons"."type" IN ("Rectangle")
Это не программная ошибка, запрос включает всех известных потомков Rectangle
.
Способом, гарантирующим что это будет работать правильно, независимо от порядка выполнения, является загрузка вручную прямых подклассов внизу файла, определяющего каждый промежуточный класс:
# app/models/rectangle.rb
class Rectangle < Polygon
end
require_dependency 'square'
Это должно произойти для каждого промежуточного (не корневого и не листового) класса. Корневой класс не охватывает запрос по типу и, следовательно, не обязательно должен знать всех своих потомки.
require
Для файлов, определяющих константы, которые необходимо автоматически загрузить, нельзя делать require
:
require "user" # НЕ НАДО ТАК
class UsersController < ApplicationController
# ...
end
Тут есть две ловушки в режиме development:
Если User
автоматически загружен до выполнения require
, app/models/user.rb
запустится снова, так как load
не обновляет $LOADED_FEATURES
.
Если require
выполнится сначала, Rails не пометит User
как автозагруженную константу, и изменения в app/models/user.rb
не будут перезагружены.
Просто используйте автозагрузку всегда, никогда не смешивайте автозагрузку и require
. В крайнем случае, если некоторому файлу абсолютно необходимо загрузить определенный файл, используйте require_dependency
, который прекрасно работает с автозагрузкой констант. Хотя это опция крайне редко необходима на практике.
Конечно, использование require
в автоматически загруженных файлах для загрузки обычных сторонних библиотек — это прекрасно, и Rails способен отличить их константы, так как они не помечены, как автозагруженные.
Рассмотрим это присваивание в config/initializers/set_auth_service.rb
:
AUTH_SERVICE = if Rails.env.production?
RealAuthService
else
MockedAuthService
end
Целью этой настройки может быть то, что приложение использует класс, соответствующий окружению, с помощью AUTH_SERVICE
. В режиме development автоматически загружается MockedAuthService
при запуске инициализатора. Допустим, мы сделали пару запросов, изменили его реализацию, и снова обратились к приложению. К нашему удивлению, изменения не отразились. Почему?
Как мы видели раньше, Rails удаляет автозагруженные константы, но AUTH_SERVICE
хранит оригинальный объект класса. Устаревший, недоступный с помощью оригинальной константы, но функционирующий.
Следующий пример обобщает ситуацию:
class C
def quack
'quack!'
end
end
X = C
Object.instance_eval { remove_const(:C) }
X.new.quack # => quack!
X.name # => C
C # => uninitialized constant C (NameError)
В связи с этим, автоматически загружать константы при инициализации приложения является плохой идеей.
В вышеприведенном случае можно реализовать динамическую точку доступа:
# app/models/auth_service.rb
class AuthService
if Rails.env.production?
def self.instance
RealAuthService
end
else
def self.instance
MockedAuthService
end
end
end
и использовать в приложении AuthService.instance
. AuthService
будет автоматически загружен по требованию и будет дружить с автозагрузкой.
require_dependency
и инициализаторыКак мы уже видели, require_dependency
загружает файлы дружественным с автозагрузкой способом. Впрочем, обычно такой вызов не имеет смысла в инициализаторе.
Кто-то может додуматься сделать вызовы require_dependency
в инициализаторе, чтобы убедиться, что некоторые константы загружены первоначально, например, как попытку решить проблему со STI.
Проблема в том, что в режиме development автозагруженные константы удаляются, если было какое-либо относящееся изменение в файловой системе. Если такое случается, мы находимся в той же самой ситуации с инициализатором, которую хотим избежать!
Вызовы require_dependency
стратегически должны быть написаны в местах автозагрузки.
Давайте рассмотрим летный симулятор. В приложении есть модель полета по умолчанию
# app/models/flight_model.rb
class FlightModel
end
которая может быть переопределена для каждого самолета, например
# app/models/bell_x1/flight_model.rb
module BellX1
class FlightModel < FlightModel
end
end
# app/models/bell_x1/aircraft.rb
module BellX1
class Aircraft
def initialize
@flight_model = FlightModel.new
end
end
end
Инициализатор хочет создать BellX1::FlightModel
и во вложенности есть BellX1
, что выглядит хорошо. Но, если модель полета по умолчанию загружена, а модель для Bell-X1 нет, интерпретатор способен разрешить FlightModel
верхнего уровня, и автозагрузка для BellX1::FlightModel
не сработает.
Этот код зависит от последовательности выполнения.
Подобный тип двусмысленности часто можно разрешить с помощью ограниченных констант:
module BellX1
class Plane
def flight_model
@flight_model ||= BellX1::FlightModel.new
end
end
end
А также, решением является require_dependency
:
require_dependency 'bell_x1/flight_model'
module BellX1
class Plane
def flight_model
@flight_model ||= FlightModel.new
end
end
end
Даны
# app/models/hotel.rb
class Hotel
end
# app/models/image.rb
class Image
end
# app/models/hotel/image.rb
class Hotel
class Image < Image
end
end
Выражение Hotel::Image
двусмысленное, так как оно зависит от последовательности выполнения.
Как мы видели раньше, Ruby ищет константу в Hotel
и его предках. Если app/models/image.rb
был загружен, но app/models/hotel/image.rb
не был, Ruby не найдет Image
в Hotel
, но найдет в Object
:
$ bin/rails runner 'Image; p Hotel::Image' 2>/dev/null
Image # НЕ Hotel::Image!
Коду, вычисляющему Hotel::Image
, нужно убедиться, что app/models/hotel/image.rb
был загружен, возможно, с помощью require_dependency
.
Хотя в таких случаях интерпретатор вызывает предупреждение:
warning: toplevel constant Image referenced by Hotel::Image
Эту удивляющую резолюцию констант можно увидеть на любом ограниченном классе:
irb(main):001:0> String::Array
(irb):1: warning: toplevel constant Array referenced by String::Array
=> Array
Чтобы эта проблема проявилась, нужно чтобы ограниченное пространство имен было классом, так как Object
не предок модулей.
Допустим, у нас имеются такие определения класса:
# app/models/hotel/services.rb
module Hotel
class Services
end
end
# app/models/hotel/geo_location.rb
module Hotel
class GeoLocation
class << self
Services
end
end
end
Если Hotel::Services
известен во время загрузки app/models/hotel/geo_location.rb
, Services
разрешится Ruby, так как Hotel
принадлежит вложенности, когда открыт синглтон-класс Hotel::GeoLocation
.
Но, если Hotel::Services
неизвестен, Rails не сможет автоматически загрузить его, приложение вызовет NameError
.
Причина в том, что автозагрузка включается для синглтон-класса, который анонимный, и, как мы уже видели, Rails проверяет только пространство имен верхнего уровня в этом крайнем случае.
Простым решением этой проблемы является ограничение константы:
module Hotel
class GeoLocation
class << self
Hotel::Services
end
end
end
BasicObject
У прямых потомков BasicObject
нет Object
среди предков, и они не могут разрешить константы верхнего уровня:
class C < BasicObject
String # NameError: uninitialized constant C::String
end
Когда вызывается автозагрузка, она работает необычно. Давайте рассмотрим:
class C < BasicObject
def user
User # НЕВЕРНО
end
end
Так как Rails проверяет пространство имен верхнего уровня, User
автоматически загрузится только в первый раз, когда вызовется метод user
. Исключение получится только когда константа User
будет известна в этом месте, в частности, при втором вызове user
:
c = C.new
c.user # к удивлению произойдет, User
c.user # NameError: uninitialized constant C::User
так как он обнаруживает, что родительское пространство имен уже имеет константу (смотрите Ограниченные ссылки.)
Как и в чистом Ruby, в теле прямых потомков BasicObject
всегда используйте абсолютные пути констант:
class C < BasicObject
::String # ПРАВИЛЬНО
def user
::User # ПРАВИЛЬНО
end
end
При настройке среды test
для автозагрузки необходимо рассматривать несколько факторов.
Например, может потребоваться запускать тесты с идентичной настройкой как в production (config.eager_load = true
, config.cache_classes = true
), чтобы поймать любые проблемы до того, как они попадут в production (это компенсация за отсутствие соответствия dev-prod). Однако это замедлит время загрузки отдельных тестов на dev машине (и не сразу совместимо со spring, подробнее смотрите ниже). Поэтому, одна из возможностей - сделать это на CI машине (которая должна запускаться без spring).
На development машине можно запускать тесты так быстро, насколько это возможно (в идеале при config.eager_load = false
).
С помощью предзагрузчика Spring (включая новые приложения Rails), идеально чтобы config.eager_load = false
в соответствии с development. Иногда можно получить гибридную конфигурацию (config.eager_load = true
, config.cache_classes = true
И config.enable_dependency_loading = true
), смотрите проблему со spring. Однако, может быть проще сохранить ту же конфигурацию, что и в development, и получить причину того, что приводит к сбою автозагрузки (возможно, по результатам тестов в CI).
Иногда может потребоваться явно использовать eager_load с помощью Rails.application.eager_load!
в настройке тестов -- это может произойти, если тесты включают многотредовость.
Active Support способен отчитываться о константах по мере их загрузки. Чтобы включить их в приложении Rails, поместите две следующие строчки в инициализатор:
ActiveSupport::Dependencies.logger = Rails.logger
ActiveSupport::Dependencies.verbose = true
Если константа Foo
была автоматически загружена, и вам хочется узнать, откуда пришла эта автозагрузка, просто введите
puts caller
вверху foo.rb
и посмотрите напечатанный отпечаток стека.
В любое время,
ActiveSupport::Dependencies.autoloaded_constants
содержит коллекцию констант, которые были автоматически загружены на настоящий момент.