November 5, 2015
When a Rails application involving multiple gems, engines etc. are built then it's important to know how constants are looked up and resolved.
Consider a brand new Rails app with model User
.
class User
def self.model_method
'I am in models directory'
end
end
Run User.model_method
in rails console. It runs as expected.
Now add file user.rb
in lib
directory.
class User
def self.lib_method
'I am in lib directory'
end
end
Reload rails console and try executing User.model_method
and User.lib_method
.
You will notice that User.model_method
gets executed and User.lib_method
doesn't. Why is that?
If you have worked in other programming languages like Python or Java then in your file you must have statements to import other files. The code might look like this.
import static com.googlecode.javacv.jna.highgui.cvCreateCameraCapture;
import static com.googlecode.javacv.jna.highgui.cvGrabFrame;
import static com.googlecode.javacv.jna.highgui.cvReleaseCapture;
In Rails we do not do that. That's because DHH does not like the idea of opening a file and seeing the top of the file littered with import statements. He likes to see his files beautiful.
Since we do not import file then how does it work?
In Rails console when user types User
then rails detects that User
constant is not loaded yet.
So it needs to load User
constant.
However in order to do that it has to load a file.
What should be the
name of the file. Here is what Rails does.
Since the constant name is User
Rails says that I'm going to look for file user.rb
.
So now we know that we are looking for user.rb
file. But the
question is where to look for that file. Rails has autoload_path
. As
the name suggests this is a list of paths from where files are
automatically loaded. Rails will search for user.rb
in this list of
directories.
Open Rails console and give it a try.
$ rails console
Loading development environment (Rails 4.2.1)
irb(main):001:0> ActiveSupport::Dependencies.autoload_paths
=> ["/Users/nsingh/code/bigbinary-projects/wheel/app/assets",
"/Users/nsingh/code/bigbinary-projects/wheel/app/controllers",
"/Users/nsingh/code/bigbinary-projects/wheel/app/models",
"/Users/nsingh/code/bigbinary-projects/wheel/app/helpers"
.............
As you can see in the result one of the folders is app/models
. When
Rails looks for file user.rb
in app/models
then Rails will find it
and it will load that file.
That's how Rails loads User
in Rails console.
Let's try to load User
from lib
directory.
Open config/application.rb
and add following code in the
initialization part.
config.autoload_paths += ["#{Rails.root}/lib"]
Now exit rails console and restart it. And now lets try to execute the same command.
$ rails console
Loading development environment (Rails 4.2.1)
irb(main):001:0> ActiveSupport::Dependencies.autoload_paths
=> ["/Users/nsingh/code/bigbinary-projects/wheel/app/lib",
"/Users/nsingh/code/bigbinary-projects/wheel/app/assets",
"/Users/nsingh/code/bigbinary-projects/wheel/app/controllers",
"/Users/nsingh/code/bigbinary-projects/wheel/app/models",
"/Users/nsingh/code/bigbinary-projects/wheel/app/helpers"
.............
Here you can see that lib
directory has been added at the very top.
Rails goes from top to bottom while looking for user.rb
file. In this
case Rails will find user.rb
in lib
and Rails will stop looking for
user.rb
. So the end result is that user.rb
in app/models
directory
would not even get loaded as if it never existed.
Here we are trying to add an extra method to User
model. If we stick
our file in lib
then our user.rb
is never loaded because Rails will
never look for anything in lib
by default. If we ask Rails to look in
lib
then Rails will not load file from app/models
because the file
is already loaded. So how do we enhance a model without sticking code in
app/models/user.rb
file.
We need some way to load User
from both models and lib directories.
This can be done by adding an
initializer to config/initializers directory with following code snippet
%w(app/models lib).each do |directory|
Dir.glob("#{Rails.root}/#{directory}/user.rb").each {|file| load file}
end
Now both User.model_method
and User.lib_method
get executed as expected.
In the above case when first time user.rb
is loaded then constant
User
gets defined. Second time ruby understands that constant is
already defined so it does not bother defining it again. However it adds
additional method lib_method
to the constant.
In that above case if we replace load file
with require file
then
User.lib_method
will not work. That is because require
will not load
a file if a constant is already defined. Read
here
and
here
to learn about how load
and require
differ.
Another approach of solving this issue is by using require_relative
inside model. require_relative
loads the file
present in the path that is relative to the file where the statement is called in. The desired file to be loaded is given
as an argument to require_relative
In our example, to have User.lib_method
successfully executed, we need to load the lib/user.rb
. Adding the following code
in the beginning of the model file user.rb
should solve the problem. This is how app/models/user.rb
will now look like.
require_relative '../../lib/user'
class User
def self.model_method
'I am in models directory'
end
end
Here require_relative
upon getting executed will first initialize the constant User
from lib directory.
What follows next is opening of the same class User
that has been initialized already and addition of model_method
to it.
In one of the projects we are using engines.
SaleEngine
has a model Sale
. However Sale
doesn't get resolved as
path for engine is neither present in config.autoload_paths
nor in ActiveSupport::Dependencies.autoload_paths
.
The initialization of engine happens in engine.rb
file present inside lib
directory of the engine.
Let's add a line to load engine.rb
inside application.rb
file.
require_relative "../sale_engine/lib/sale_engine/engine.rb"
In Rails console if we try to see autoload path then we will see that
lib/sale_engine
is present there. That means we can now use
SaleEngine::Engine
.
Now any file we add in sale_engine
directory would be loaded. However
if we add user.rb
here then the user.rb
mentioned in app/models
would be loaded first because the application directories have
precedence. The precedence order can be changed by following statements.
engines = [SaleEngine::Engine] # in case there are multiple engines
config.railties_order = engines + [:main_app]
The symbol :main_app
refers to the application where the server comes up. After adding the above code, you will see that
the output of ActiveSupport::Dependencies
now shows the directories of engines first (in the order in which they have been
given) and then those of the application. Hence for any class which is common between your app and engine, the one from
engine will now start getting resolved. You can experiment by adding multiple engines and changing the railties_order
.
Loading of constants is a big topic and Xavier Noria from Rails core team has made some excellent presentations. Here are some of them
We have also made a video on How autoloading works in Rails.
If this blog was helpful, check out our full blog archive.