How constant lookup and resolution works in Ruby on Rails

来源:转载


November 05, 2015 - Mohit Natoo

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' endend

Run User.model_methodin rails console. It runs as expected.

Now add file user.rbin libdirectory.

class User def self.lib_method 'I am in lib directory' endend

Reload rails console and try executing User.model_methodand User.lib_method. You will notice that User.model_methodgets executed and User.lib_methoddoesn’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.

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 DHHdoes 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 it works.

In Rails console when user types Userthen rails detects that Userconstant is not loaded yet. So it needs to load Userconstant. 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 UserRails says that I’m going to look for file user.rb.

So now we know that we are looking for user.rbfile. 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.rbin this list of directories.

Open Rails console and give it a try.

$ rails consoleLoading 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.rbin app/modelsthen Rails will find it and it will load that file.

That’s how Rails loads Userin Rails console.

Adding lib to the autoload path

Let’s try to load Userfrom libdirectory. Open config/application.rband add following code in the initialization part.

config.autoload_paths += ["#{Rails.root}/lib"]

Now exit rails console and start rails console. And now lets try to execute the same command.

$ rails consoleLoading 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 libdirectory has been added at the very top. Rails goes from top to bottom while looking for user.rbfile. In this case Rails will find user.rbin liband Rails will stop looking for user.rb. So the end result is that user.rbin app/modelsdirectory would not even get loaded as if it never existed.

Enhancing a model

Here we are trying to add an extra method to Usermodel. If we stick our file in libthen our user.rbis never loaded because Rails will never look for anything in libby default. If we ask Rails to look in libthen Rails will not load file from app/modelsbecause the file is already loaded. So how do we enhance a model without sticking code in app/models/user.rbfile.

Introducing initializer to load files from model and lib directories

We need some way to load Userfrom both models and lib directories. This can be done by adding an initializer to config/initializersdirectory 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_methodand User.lib_methodget executed as expected.

In the above case when first time user.rbis loaded then constant Usergets defined. Second time ruby understands that constant is already defined so it does not bother defining it again. However it adds additional method lib_methodto the constant.

In that above case if we replace load filewith require filethen User.lib_methodwill not work. That is because requirewill not load a file if a constant is already defined. Read hereand hereto learn about how loadand requirediffer.

Using ‘require_relative’ in model

Another approach of solving this issue is by using require_relativeinside model. require_relativeloads 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_methodsuccessfully executed, we need to load the lib/user.rb. Adding the following code in the beginning of the model file user.rbshould solve the problem. This is how app/models/user.rbwill now look like.

require_relative '../../lib/user' class User def self.model_method 'I am in models directory' end endend

Here require_relativeupon getting executed will first initialize the constant Userfrom lib directory. What follows next is opening of the same class Userthat has been initialized already and addition of model_methodto it.

Handling priorities between Engine and App

In one of the projects we are using engines. SaleEnginehas a model Sale. However Saledoesn’t get resolved as path for engine is neither present in config.autoload_pathsnor in ActiveSupport::Dependencies.autoload_paths. The initialization of engine happens in engine.rbfile present inside libdirectory of the engine. Let’s add a line to load engine.rbinside application.rbfile.

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_engineis present there. That means we can now use SaleEngine::Engine.

Now any file we add in sale_enginediretory would be loaded. However if we add user.rbhere then the user.rbmentioned in app/modelswould 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 enginesconfig.railties_order = engines + [:main_app]

The symbol :main_apprefers to the application where the server comes up. After adding the above code, you will see that the output of ActiveSupport::Dependenciesnow 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 Noriafrom Rails core team has made some excellent presentations. Here are some of them

Constant Autoloading in Ruby on RailsBaruco 2013 Constants in RubyRuLu 2012 Class Reloading in Ruby on RailsRailsConf 2014

We have also made a video on How autoloading works in Rails.



分享给朋友:
您可能感兴趣的文章:
随机阅读: