Read more

Manually requiring your application's models will lead to trouble

Arne Hartherz
October 19, 2012Software engineer at makandra GmbH

In a nutshell:

If you require your Rails models manually, pay attention to the path you use. Unless you have to, don't do it at all.

Background

Illustration online protection

Rails Long Term Support

Rails LTS provides security patches for old versions of Ruby on Rails (2.3, 3.2, 4.2 and 5.2)

  • Prevents you from data breaches and liability risks
  • Upgrade at your own pace
  • Works with modern Rubies
Read more Show archive.org snapshot

Consider these classes:

# app/models/user.rb

class User < ActiveRecord::Base
  validate :magic

  def magic
    errors.add_to_base('failed') if bad_things?
  end
end

^
# app/models/foo.rb

require 'user'

class Foo
  # something happens here
end

Now, when your environment is booted, Rails will automatically load your models, like User.

In our example, when Foo gets loaded, it will require the User class itself again. Since Ruby 1.8 identifies required files by the string you used (as you alread know), this will mean the User class is required a second time.

This, in turn, will lead to the effect that User#magic is called twice when a User record is validated.

Note that loading classes a second time will lead to all kinds of trouble. Stay away from that, Kids!

Fixing it

You have two options:

  1. Don't do it. Rails will load your models, so there is almost no reason to load it yourself.

  2. If you absolutely have to do it, use the path that Rails would use. In my case, the only correct answer would have been:

    require File.join(Rails.root, 'app/models/user')
    

It gets worse

The problem gets more strange when this issue "magically" appears or disappears.

If the Foo class from our example is not being auto-loaded, all will be fine for the most time, but you will encounter validation errors being added twice occasionally.

This happened to me when during tests: a spec was referring to Foo, which was not yet loaded, because it was in lib/foo.rb. When the spec was run, Rails loaded the class the first time it was referred to, causing user to be loaded, which Ruby believed it had not seen before. And that caused a completely different spec to fail at a later point of the spec run, because now User ran its manual validation method twice.

Consider the fun you'll have when running tests in parallel.

Posted by Arne Hartherz to makandra dev (2012-10-19 17:46)