After researching this on SO and a very similar issue in Rails' GitHub issues, I'm still unclear what's wrong. My namespaced model subclasses are not eager-loaded, but I believe they are declared correctly and in the right place.
They do seem to be autoloaded and are accessible, but each one does not show up in subclasses of the parent class until they are instantiated.
The parent model:
# /app/models/queued_email.rb
class QueuedEmail < ApplicationRecord
end
My namespaced subclass models (there are a dozen):
# /app/models/queued_email/comment_notification.rb
class QueuedEmail::CommentNotification < QueuedEmail
end
# or alternatively (this also doesn't eager load):
module QueuedEmail
class CommentNotification < QueuedEmail
end
end
The relevant message from Rails.autoloaders.log! (in config/application.rb)
Zeitwerk#rails.main: autoload set for QueuedEmail, to be autovivified from /vagrant/rails_app/app/models/queued_email
Zeitwerk#rails.main: earlier autoload for QueuedEmail discarded, it is actually an explicit namespace defined in /vagrant/rails_app/app/models/queued_email.rb
Zeitwerk#rails.main: autoload set for QueuedEmail, to be loaded from /vagrant/rails_app/app/models/queued_email.rb
If I open rails console and call subclasses, i get nothing:
> QueuedEmail
=> QueuedEmail (call 'QueuedEmail.connection' to establish a connection)
> QueuedEmail.subclasses
[]
But then... the subclass is accessible.
> QueuedEmail::CommentNotification
=> QueuedEmail::CommentNotification(id: integer...)
> QueuedEmail::CommentNotification.superclass
=> QueuedEmail(id: integer...)
> QueuedEmail.subclasses
=> [QueuedEmail::CommentNotification(id: integer...)]
I get nothing in subclasses until each one is instantiated in the code. Is my app/models folder incorrectly organized, or my subclasses incorrectly named?
Let me first explain the log messages.
Zeitwerk scans the project, and found a directory called queued_email before finding queued_email.rb. So, as a working hypothesis it assumed QueuedEmail was an implicit namespace with the information that it had. This hypothesis got later invalidated when it saw queued_email.rb, and said "wait, this is actually an explicit namespace". So it undid the implicit setup, and redefined it to load an explicit namespace.
Now, let's go for the subclasses.
When an application does not eager load, files are only loaded on demand. For example, if you load QueuedEmail, and app/models/queued_email has 24 files recursively, none of them are loaded until they are used.
When a class is subclassed, the collection returned by subclasses is populated. But you don't know a class is subclassed until the subclass is loaded. Therefore, in a lazy loading environment subclasses is empty at the start. If you load 1 subclass it will have that one, but not the rest, until they are all eventually loaded.
If you need the subclasses to be there for the application to function properly, starting with Zeitwerk 2.6.2 you can throw this to an initializer
# config/initializers/eager_load_queued_email.rb
Rails.application.config.to_preprare do
Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/queued_email")
end
Related
I am working on a Ruby on Rails 6 project, and I am trying to use a class instance variable on an ActiveRecord model. Here is a basic example:
class Model << ApplicationRecord
#var = AnotherClass.new
class << self
attr_reader :var
end
# ...
end
I would then like to be able to use Model.var to access Model's instance of AnotherClass. There are multiple such models, each of them referring to a different AnotherClass, with all the AnotherClasses being subclasses of some BaseClass.
However, I am encountering the following error:
uninitialized constant Model::AnotherClass
Because of the class << self, Ruby seems to be looking for a nested class.
Is there a way to access AnotherClass directly, or is there a better way in general to set this up?
Edit: I solved this with a completely different approach, however I'm still interested to see how you would get around this issue.
The error you receive:
uninitialized constant Model::AnotherClass
Tells you that AnotherClass is not initialized (not loaded/found). Let me use the following context as an example:
class Model
AnotherClass
end
Ruby will start a constant lookup. This will start from the current namespace (Model) and and if nothing is found move up into the namespace tree. In the above example it will first look for Model::AnotherClass if that cannot be found it will look for AnotherClass, if that cannot be found it will throw the exception you receive.
This error simply tells you that AnotherClass is not loaded.
Anything in th app/ directory is loaded by the autoloader of Rails, however if you use the lib/ directory you have to manually require 'another_class' or add the relevant path to the autoload paths.
In my database I have a table people, and I'm using single table inheritance, with these classes:
class Person < ActiveRecord::Base
end
class Member < Person
end
class Business < Member
end
The queries it generates confuse me. What I want is for Member.all to return all Businesses as well as any other subtypes of Member. Which it does, but only if I've accessed the Business class recently. I assume it's because my classes aren't being cached in development mode (for obvious reasons), but it still seems like strange/buggy behaviour.
Is this a bug in rails? Or is it working as intended? In either case, can anyone think of a good fix for development purposes?
This is intentional behaviour—the official Rails guide on Autoloading and Reloading Constants explains it pretty well in the section on Autoloading and STI:
…
A way to ensure this works correctly regardless of the order of
execution is to load the leaves of the tree by hand at the bottom of
the file that defines the root class:
# app/models/polygon.rb
class Polygon < ApplicationRecord
end
require_dependency 'square'
Only the leaves that are at least grandchildren need to be loaded this
way. Direct subclasses do not need to be preloaded. If the hierarchy
is deeper, intermediate classes will be autoloaded recursively from
the bottom because their constant will appear in the class definitions
as superclass.
So in your case, this would mean putting an require_dependency "business" at the end of your Person class.
However, beware of circular dependencies which can possibly be avoided by using require instead of require_dependency (even though it may prohibit Rails from tracking and reloading your files when changes are made—after all, require_dependency is a Rails-internal method).
By default, Rails is not eager loading your classes in development. Try changing the following line in your config/environments/development.rb:
# Do not eager load code on boot.
config.eager_load = false
to:
# Do eager load code on boot!
config.eager_load = true
This question already has an answer here:
`ClassName.constants` returning empty array in Rails app
(1 answer)
Closed 7 years ago.
I know that Rails has an opinion on what the name of my classes and modules should be. As such, I have tried to cohere with it.
In Rails.root/lib/query_finder directory, I have the following structure:
/lib
/query_finder
/adapters
active_record.rb
mongoid.rb
base.rb
/strategies
base.rb
In base.rb, I named my class like so:
module QueryFinder
class Base
end
end
In adapters/base.rb, I named my class like so:
module QueryFinder
module Adapters
class Base
end
end
end
In adapters/mongoid.rb, I named my class like so:
module QueryFinder
module Adapters
class Mongoid
end
end
end
In adapters/active_record.rb, I named my class like:
module QueryFinder
module Adapters
class ActiveRecord
end
end
end
But Rails is unable to find the adapters. I try to grab all the constants:
> QueryFinder::Adapters.constants
=> []
And it's giving me an empty array. I also added the following to autoload path:
config.autoload_paths += Dir["#{config.root}/lib/**/"]
What might I be doing wrong?
I just want to make a note that I am able to reference the constants and classes like so:
QueryFinder::Adapters::Base
=> QueryFinder::Adapters::Base
The problem is when I use the constants method, it gives an empty array.
The problem is lazy loading. Rails, in production mode, is usually set up to do lazy loading. That means that the constants are not actually defined until you reference them: At that time it notices you're referencing an undefined constant, finds the file, and loads it.
If you turned off lazy loading, then you could rely upon .constants to tell you what was available. But performance would suffer.
Another workaround would be for you to explicitly load the required files.
I have two namespaces, each with its own controller and presenter classes:
Member::DocumentsController
Member::DocumentPresenter
Guest::DocumentsController
Guest::DocumentPresenter
Both presenters inherit from ::DocumentPresenter.
Controllers access their respective presenters without namespace specified, e.g.:
class Guest::DocumentsController < ActionController::Base
def show
DocumentPresenter.new(find_document)
end
end
This usually calls presenter within same namespace. However sometimes in development environment I see base ::DocumentPresenter is being used.
I suspect the cause is that base ::DocumentPresenter is already loaded, so Rails class auto-loading doesn't bother to look further. Is this likely the case? Can it happen in production environment too?
I can think of two solutions:
rename base class to DocumentPresenterBase
explicitly require appropriate presenter files in controller files
Is there a better solution?
You are correct in your assumptions - If you do not specify namespace, Ruby starts from current namespace and works its way up to find the class, and because the namespaced class is not autoloaded yet, the ::DocumentPresenter is found and autoloader does not trigger.
As a solution I would recommend renaming ::DocumentPresenter to DocumentPresenterBase, because this protects you from bugs when you forget namespacing or explicit requiring somewhere.
The second option to consider would actually be using specific namespaced classnames all over the place, but this suffers from bugs when you accidentally forget to namespace some call.
class Guest::DocumentsController < ActionController::Base
def show
Guest::DocumentPresenter.new(find_document)
end
end
Third option would be your second - explicitly require all the classes in initializer beforehand. I have done this with Rails API which receives embedded models in JSON and Rails tends to namespace them when the actual models are not loaded yet.
Option 3.5 You could probably trick autoloader to do the heavy lifting (though, this might seem more like a hack):
class Guest::DocumentsController < ActionController::Base
# trigger autoload
Guest::DocumentPresenter
def show
# This should refer Guest::DocumentPresenter
DocumentPresenter.new(find_document)
end
def show
# As will this
DocumentPresenter.new(find_document)
end
end
Still the cleanest would be to rename the base class.
I think in 3 solutions if you want to mantein the name, one is your second solution.
1) explicitly require appropriate presenter files in controller files
2) Execute the full environment class path, like:
class Guest::DocumentsController < ActionController::Base
def show
Guest::DocumentPresenter.new(find_document)
end
end
3) Create a file on initialize directory and execute require manually (the worst options :S)
Let's say I have something like this
class Major < ActiveRecord::Base
def self.my_kids
self.subclasses.collect {|type| type.name}.sort
end
end
class MinorOne < Major
end
class MinorTwo < Major
end
In Rails 2.3 I could call Major.my_kids and get back an array of the subclass names, but in Rails 3.0.3 I get back an empty array, unless I load the subclasses first. This seems wrong to me, am I missing something or is this new to Rails 3?
There's no difference that I know of between Rails 2 and 3 concerning the use of the subclasses method. You might have believed it was working previously because the subclasses were already loaded. As Rails load most files dynamically a parent class couldn't know about any class derived from it unless it is defined in the same file. The easiest way to ensure all models are loaded, you can simply call require on all files in the app/models directory:
Dir.glob(RAILS_ROOT + '/app/models/*.rb').each { |file| require file }
One other thing to note is that the subclasses method doesn't work after issuing the reload! command in the Rails console.
The reason why you're getting an empty array in Rails 3 is likely because Rails 3 uses autoloading.
If you open the Rails console and you reference the name of the subclasses, then run your 'subclasses' method on the parent class, you'll see it works. This is because Rails 3 only loads the classes into memory that you've referenced, when you reference them.
The way I ended up forcing my classes to load from a library I created under /lib was with the following code I added into the method that depends on those classes:
# load feature subclasses
my_classes_path = File.expand_path(File.dirname(__FILE__)) + "/my_classes"
if File.directory?(my_classes_path)
Dir.glob(my_classes_path + "/*.rb").each do |f|
load f
end
end