Rails 4.2 class loading

11 May 2015

Supposing you have a Rails 4.2 application with a Book model, and there's a record of who has read that book, so it has_many :readings. If you start a new console, reopen the class, and try to use that association, you might be surprised by the result:

irb(main):001:0> class Book < ActiveRecord::Base ; end
=> nil
irb(main):002:0> Book.first.readings
  Book Load (0.7ms)  SELECT  "books".* FROM "books"  ORDER BY "books"."id" ASC LIMIT 1
NoMethodError: undefined method `readings' for #<Book:0x007fef076616d0>
	from gems/activemodel-4.2.1/lib/active_model/attribute_methods.rb:433:in `method_missing'
	from (irb):2

What? Where did my associations go?

This puzzled me for a while until I realized that the first line of code in that irb session is not reopening the class. Instead, it's defining it for the first time, and once Rails has a class definition in place it won't attempt to load the definition that's in app/models/.

This bit me recently on a Rails 4.1 to 4.2 upgrade. I was running the test suite and getting errors as if fixtures were not working - i.e., errors like ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'readings' in 'field list':. But it was because I was reopening model classes in test_helper to add some methods, and those reopenings didn't have the full class definitions with all the associations and module inclusions and whatnot. Yup, I know, bad practice, hey, reasons. So I "fixed" it by adding Book (thanks to Michael Cohen for noting that Book.first did an unnecessary query) just before my class Book < ActiveRecord::Base in test_helper; with that in place the app/models/book.rb class definition was loaded first and then my reopening code was actually reopening as I intended.

Joe Rafaniello had an even better idea. Rather than using autoloading by referencing the constant, just do a plain old require 'book'; that explicitly shows that we're loading the class rather than trying to fool autoloading.

More generally, Michael Cohen also pointed to Justin Weiss's article on monkeypatching and suggested putting these test-related monkeypatches in a module rather than just slapping them into test_helper.rb.