Monday, May 22, 2006

When Rails Needs a Clue - Single Table Inheritance Problems

Given Grandchild < Child < Parent < ActiveRecord::Base, your controllers need model :grandchild. Without it, well, this could happen:

There is a sinking feeling in your gut. You've been working in rails for weeks. You've been doing everything right, Test Driven Development to the hilt, when suddenly, the worst happens. Behavior in test doesn't match behavior in development. You begin to question whether any of your testing is valid. Does your app really work, or does it just seem to work in test? How can test and development possibly differ? Your worst nightmares are coming true.

This might seem like a ridiculous scenario but it is exactly what I have dealt with for the last week. We have a table with single table inheritance. Admin < Broker < User < ActiveRecord::Base all in table :users. Errors started showing up in development that passed in test. We tracked it down to differing sql statements. In test, Broker.find_all generated SELECT * FROM users WHERE ( (users.`type` = 'Broker' OR users.`type` = 'Admin' ) ). In development, the exact same Broker.find_all generated SELECT * FROM users WHERE ( (users.`type` = 'Broker' ) ).

We had no idea what was going on and we had features that had to be ready right now for a demo. We worked around the problem. We wrote our own find conditions and moved on, writing a todo to find out what was wrong. The sleepless nights started and the worry began to grow.

Over the weekend, the other developer on my team looked at some code I wrote Friday and had problems. He didn't have time to look into it but told me about it this morning. My first response, well, I'll write a test that shows the problem, only every test I wrote worked fine in test. I could reproduce his problems in development every time. This shouldn't be so hard.

Start from the basics. I created a test with exactly the same input as was reported in the error on development. Same parameters. Same session. Same flash. Everything was exactly the same. Only, what failed in development passed in test. The sick feeling in my stomach is getting stronger. I feel like I'm in a dream watching my career go down the drain. Sure enough, I track it down to nearly the same problem as before. We have Default < ActiveRecord::Base; belongs_to Broker. If the default.broker_id pointed to a user with type=Admin, it failed in development. Default.broker was returning nil when it should have been returning an Admin. Again, the SQL was different between test and development in exactly the same way. Development would only accept Brokers and test accepted Brokers and Admins. This didn't make any sense and this time, we had time to look at it.

My partner started looking at the source in Rails and I started searching online. I didn't find anything with a few quick searches. I knew the #rubyonrails channel on Freenode is pretty active so I decided to ask my question there. "Has anyone seen differences between test and development when..." Hmm. I want the exact SQL I'm seeing in test and development before I ask this question. I know, I'll fire up the console for both and get it from the logs that way.

Loki took pity on me here and Coyote found something else to toy with. export RAILS_ENV=test; ./script/console. So far, so good. Broker.find_all; I only get Brokers back. WTF! That's the same thing I've been seeing in development. A quick check. Yes, I'm in test. Hmm. User.find_all; I get everything. (select * from users, no where clause) Admin.find_all; I get only Admins. I expected that, Admin doesn't have any children. Broker.find_all; Holy hand grenades. That is everything, Admins included. The where clause changed. Umm. But. Maybe this time I put something different. Just to be sure, I used up arrow to run the first Broker.find_all; (I couldn't see a difference but who knows at this point.) Sure enough, the original search turns up the new results. Then, wham, lightning struck. If Admin has never been loaded, Rails doesn't know Admin < Broker!

A quick conversation with my partner, a little test (added model :admin to the controller) and sure enough, everything is peachy. There is just one nagging doubt. I don't know how to create this in test. You see, single table inheritance is implemented in a single YAML fixture. When fixtures :users runs, Rails figures out I have a User, Broker, Customer (< User) and Admin model. Loading my fixture seems to make Rails aware of those relationships. So, how do I get data for admins into the table for users without making Rails aware of the Admin class? I have no idea.

The sinking feeling isn't gone but it is a good deal smaller. I now have a picture of the world that explains the unexplainable and test and development work the same again. But still, that little doubt.

If you know how to test this, won't you please let me in on the secret?

11 Comments:

Blogger Ryan Platte said...

A separate Rake task for such tests? Each Rake task fires up a completely new interpreter, so you could add back in some confidence that way. I don't remember to what extent or how often Rails unloads dependencies within a testing task, if at all.

Thank you for writing that all down. I feel that Rails's full-stack testing paradigm and the resultant design problems are its very ugliest aspects. To me they really stomp all over the wonderful wins Rails gives me in so many other areas. Defaults that are hard to override == assumptions == problems like you experienced.

12:54 PM  
Anonymous Anonymous said...

Just wanted to say thanks for writing this up. I've just spent a couple of hours with similar problems, and this has solved my difficulties!


Michael Nielsen

11:19 PM  
Blogger Glenn Nilsson said...

Thank you Sean, and Mike!

I had this exact problem also.
And I get the deprecation warnings for Rails 2.0 and a link to the RoR-site, but nothing there (yet) about it.

require_dependency 'model'

was what I needed.

2:01 AM  
Anonymous Anonymous said...

We were having this problem ourselves using single table inheritance, and simply added require_dependecy 'SubClass' for each of our subclasses. Now we can get the same functionality from script/console and the web interface... what a nightmare you saved us from!

11:47 PM  
Blogger Hique said...

This should be fixed in Rails. I spent hours in this

9:12 AM  
Blogger Cory said...

Thanks Sean and Mike!

Just wanted to clarify on Mike's comment. What worked for me was adding
require_dependency 'my_subclass'
for all of my inherited classes at the top of the ApplicationController definition

10:37 PM  
Blogger tim.linquist said...

Thank you very much for posting this. I have a similar issue where I have a Breadcrumb which is the super of Mark/Measure classes. When I queried an association (object.breadcrumbs) it was only generating SQL for breadcrumbs not the child types. Your fix has saved me from rewriting 3 days worth of code that I already rewrote once to create the architecture my site currently has. THANK YOU VERY MUCH FOR POSTING THIS! :)

6:40 PM  
Blogger tim.linquist said...

A follow up to my previous comment. I decided it is better to just override Rails internal loading engine and require all of my models explicitly in my environment.rb. I am loading all in the proper sequence with a require statement and this appears to be working fine for both controller requests and just interacting with the various models in the console. Cheers!

7:02 PM  
Anonymous Paul said...

I didn't see this explicitly mentioned, but this happens because Rails, by default, loads all classes at boot for the test (and production) environments. But the development environment lazy loads classes. This is generally correct. Lazy loading classes in test/production is too slow. But in development mode it makes it a bit easier to change your code without having to restart your webserver.

As far as testing this goes, you have three options:
1) In config/environments/test.rb, set config.cache_classes = false (like for the dev environment). I'm not 100% sure this will fix it, but I think it will.
2) Run your tests in the development environment. This could get awkward. I haven't tried it.
3) In scenarios where lazy loading causes bugs/errors, explicitly require the classes you need. This is the option I recommend. I think this is the "ruby way" to fix the problem.

A nice article you should read: http://code.alexreisner.com/articles/single-table-inheritance-in-rails.html

11:26 AM  
Anonymous Anonymous said...

Thanks this helped me solve my problem why the 'subclasses' method doesn't work as expected in the development environment.

-- Daniel

8:47 AM  
Anonymous Anonymous said...

Thanks for writing this! In general STI can be messy. Another nice post describing how to avoid creating additional columns for child classes using RoR 4 and PostgreSQL 9.3 (examples based on social media users data):
https://netguru.co/blog/posts/renewed-life-for-sti-with-postgresql-json-type

1:16 AM  

Post a Comment

<< Home