Legacy Database Column Names in Rails 3


If you work with legacy databases, you don’t always have the option of changing column names when something conflicts with Ruby or Rails. A very common example is having a column named “class” in one of your tables. Rails *really* doesn’t like this, and like the wife or girlfriend who really hates your new haircut, it will complain at every possible opportunity:

# trying to set the poorly named attribute
ruby-1.9.2-p0 > u = User.new :class => '1995'
NoMethodError: undefined method `columns_hash' for nil:NilClass
# trying to set a different attribute that is only guilty by association
ruby-1.9.2-p0 > u = User.new :name
NoMethodError: undefined method `has_key?' for nil:NilClass
# trying to set the attribute later in the game
ruby-1.9.2-p0 > u = User.new
 => #<User id: nil, name: nil, class: nil, created_at: nil, updated_at: nil> 
ruby-1.9.2-p0 > u.class = '1995'
NoMethodError: undefined method `private_method_defined?' for nil:NilClass

Like the aforementioned wife/girlfriend, you’re not going anywhere until this issue is resolved. Luckily, Brian Jones has solved this problem for us with his gem safe_attributes. Rails automatically creates accessors (getter and setter methods) for every attribute in an ActiveRecord model’s table. Trying to override crucial methods like “class” is what gets us into trouble. The safe_attributes gem turns off the creation of any dangerously named attributes.

Just do this:

# app/models/user.rb
class User < ActiveRecord::Base
  bad_attribute_names :class
end

After including the gem in your bundler, pass bad_attribute_names the list of offending column names, and it will keep Rails from trying to generate accessor methods for it. Now, this does come with a caveat: you don’t have those accessors. Let’s try to get/set our :class attribute:

ruby-1.9.2-p0 > u = User.new
 => #<User id: nil, name: nil, class: nil, created_at: nil, updated_at: nil> 
ruby-1.9.2-p0 > u.class = '1995'
 => "1995" 
ruby-1.9.2-p0 > u
 => #<User id: nil, name: nil, class: "1995", created_at: nil, updated_at: nil> 
ruby-1.9.2-p0 > u.class
 => User(id: integer, name: string, class: string, created_at: datetime, updated_at: datetime) 

The setter still works (I’m guessing that it was still created because there wasn’t a pre-existing “class=” method) and we can verify that the object’s attribute has been properly set. But calling the getter defaults to…well, the default behavior.

The answer is to always use this attribute in the context of a hash. You can send the object a hash of attribute names/values, and that works. This means your controller creating and updating won’t have to change. Methods like new, create, update_attribute, update_attributes, etc will work fine.

If you want to just set the single value (to prevent an immediate save, for example) do it like this:

ruby-1.9.2-p0 > u[:class] = '1996'
 => "1996" 
ruby-1.9.2-p0 > u
 => #<User id: nil, name: nil, class: "1996", created_at: nil, updated_at: nil> 

Basically, you can still set the attribute directly, instead of going through the rails-generated accessors. But we’re still one step away from a complete solution. We want to be able to treat this attribute like any other, and that requires giving it a benign set of accessors (getter and setter methods). One reason to do this is so we can use standard validations on this attribute.

Adding accessors to our model is this simple:

# add to app/models/user.rb

def class_name= value
  self[:class] = value
end
  
def class_name
  self[:class]
end

We’re calling the accessors “class_name”, and now we can use that everywhere instead of the original attribute name. We can use it in forms:

# example, not found in code

<%= f.text_field :class_name %>

Or in validations:

# add to app/models/user.rb

validates_presence_of :class_name

Or when creating a new object:

# example, not found in code

User.create :class_name => 'class of 1995'

If you download the code, these additions are test-driven, meaning I wrote the tests for those methods before writing the methods themselves, to be sure they worked properly. I encourage you to do the same.

Good luck!

About these ads

Tags: , , , , , , , , , ,

11 Responses to “Legacy Database Column Names in Rails 3”

  1. DGM Says:

    Wouldn’t it help to combine this with a set of virtual accessors, that write to the correct column behind the scenes?

  2. Jaime Bellmyer Says:

    DGM: There’s a built-in rails method called alias_attribute that does exactly that, but it doesn’t help in this situation. If you overwrite the “class” instance method in the example above, things will start breaking. It’s a method other parts of the app expect to work a certain way.

    That said, maybe you’re onto something. If safe_attributes accepted an “alias” name like alias_attribute does, it could create accessors with that name, that get/set values in the attributes hash directly. That’s not a bad idea at all, and seems like it’d be a great addition to safe_attributes!

  3. Brian Jones Says:

    Creating a virtual attribute that has a non-infringing name is so trivial I decided not to add that functionality some time ago. Keep in mind in your example above that you should not need to actually call bad_attribute_names at all in this particular case because by default it wouldn’t create that or anything else matching an instance method of ActiveRecord::Base anyway. That method is there as a just-in-case something else causes issues. The readme for the gem shows how to setup and use this in and out of Rails 3, see https://github.com/bjones/safe_attributes.

  4. Jaime Bellmyer Says:

    Hi Brian, thanks for commenting. I did actually have to add the more explicit call to bad_attribute_names in my example. Omitting that call resulted in the same errors as before. I haven’t had a chance to figure out why yet, since this article was the result of a question I just received in my inbox this morning.

    As for a non-conflicting virtual attribute, I respectfully disagree. It’s the reason the alias_attribute method exists in the first place – one line of code to replace six. Is it necessary? No. Most people understand the goings on well enough to do it themselves. But it *does* complete the process of a fully functioning workaround for incompatible attribute names. From here, the developer (and others that follow) never have to give the original attribute name a second thought. To me, that provides more value than saving a few lines of code – it saves mental cpu cycles, and simplifies the readability of the model.

  5. Brian Jones Says:

    If you can explain how to recreate this issue or have a test case showing this behavior then please let me know via github. Thanks and I’m glad the gem is helping people.

  6. J Says:

    Just wanted to say that I really appreciate both this writeup and the gem itself. So big thanks to Brian and Jaime. You saved my ass big time with this one!

  7. J Says:

    Quick question – so how does this work when you want to use validates :class … in the model? How do we tell it to access class through the hash? Or is there a better way to validate class in this special situation?

  8. Jaime Bellmyer Says:

    J – thanks, that’s a very good point. DGM touched on this in the comments above, so it’s obviously a key part I managed to gloss over the first time around. I updated the article and code to show how to do this.

    I’m actually a little embarrassed I didn’t add this in the first place – it really completes the solution nicely!

  9. J Says:

    Awesome, thanks for the tip!

  10. Brian Jones Says:

    I took a look at the code and removing the bad_attribute_names call in User does not cause a subsequent error when using the model. Not sure at this point why you had an issue, but I invite you to recheck it again sometime.

    I’ve also released version 1.0.3 of safe_attributes to address validations. You can use the column name as usual without creating the attribute accessors now. I haven’t tested all possible validations, but expect they all should work.

    validates_presence_of :class # will work now

    See README.rdoc for details.

  11. Brian Jones Says:

    Found your issue to be that I needed to make a few updates to support Ruby 1.9.2. Version 1.0.4 of safe_attributes should now work as advertised for ruby 1.8.7 and 1.9.2. Enjoy.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: