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.
1class User 2 def self.model_method 3 'I am in models directory' 4 end 5end
Run User.model_method in rails console. It runs as expected.
Now add file user.rb in lib directory.
1class User 2 def self.lib_method 3 'I am in lib directory' 4 end 5end
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?
In Rails we do not import files
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.
1import static com.googlecode.javacv.jna.highgui.cvCreateCameraCapture; 2import static com.googlecode.javacv.jna.highgui.cvGrabFrame; 3import 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.
1$ rails console 2Loading development environment (Rails 4.2.1) 3irb(main):001:0> ActiveSupport::Dependencies.autoload_paths 4=> ["/Users/nsingh/code/bigbinary-projects/wheel/app/assets", 5"/Users/nsingh/code/bigbinary-projects/wheel/app/controllers", 6"/Users/nsingh/code/bigbinary-projects/wheel/app/models", 7"/Users/nsingh/code/bigbinary-projects/wheel/app/helpers" 8.............
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.
Adding lib to the autoload path
Let's try to load User from lib directory. Open config/application.rb and add following code in the initialization part.
1config.autoload_paths += ["#{Rails.root}/lib"]
Now exit rails console and restart it. And now lets try to execute the same command.
1$ rails console 2Loading development environment (Rails 4.2.1) 3irb(main):001:0> ActiveSupport::Dependencies.autoload_paths 4=> ["/Users/nsingh/code/bigbinary-projects/wheel/app/lib", 5"/Users/nsingh/code/bigbinary-projects/wheel/app/assets", 6"/Users/nsingh/code/bigbinary-projects/wheel/app/controllers", 7"/Users/nsingh/code/bigbinary-projects/wheel/app/models", 8"/Users/nsingh/code/bigbinary-projects/wheel/app/helpers" 9.............
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.
Enhancing a model
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.
Introducing initializer to load files from model and lib directories
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
1%w(app/models lib).each do |directory| 2 Dir.glob("#{Rails.root}/#{directory}/user.rb").each {|file| load file} 3end
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.
Using 'require_relative' in model
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.
1require_relative '../../lib/user' 2 3class User 4 def self.model_method 5 'I am in models directory' 6 end 7end
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.
Handling priorities between Engine and App
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.
1require_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.
1engines = [SaleEngine::Engine] # in case there are multiple engines 2config.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.
Further reading
Loading of constants is a big topic and Xavier Noria from Rails core team has made some excellent presentations. Here are some of them
- Constant Autoloading in Ruby on Rails Baruco 2013
- Constants in Ruby RuLu 2012
- Class Reloading in Ruby on Rails RailsConf 2014
We have also made a video on How autoloading works in Rails.