Tuesday 22 September 2009

Gotchas for Models

I have hit a few issues using Rails, some very frustrating. Here are a selection that relate to the models.

Constructors with Arguments
The first is how to handle a constructor that will take a parameter. In Ruby, when you call new for a class, the object is created, then the initialize method is invoked. If you want to do that for your model, then you need to invoke the initialize method in the superclass to ensure everything is set up, and that is done through the super keyword.

The problem is that super takes with it all the parameters that were sent to the method. If your initialize takes two parameters, both parameters get sent to the initialize in ActiveRecord::Base, which throws an exception, because it expects just one; a hash. The solution is to use brackets with super; super().

On a kind of related note, see this page about why overriding initialize may not be the best solution, as Rails sometimes creates objects another way.
http://blog.dalethatcher.com/2008/03/rails-dont-override-initialize-on.html

Column Values
The second gotcha is accessing column values within the model. When you are not in the model, it is very simple:
@post.title = 'My new title'
s = @post.body

Ruby and Rails work together to make this seem as though you are accessing a variable in a class. However, the reality is that you are invoking methods, and this runs into problems when you do it from inside the class. You might imagine that this would work:
class Post < ActiveRecord::Base
def initialize params = {}
body = params[:body]
s = body
title = "Post: #{s[0..100]}"
end
end

It does not. The s = body line is fine; I guess Ruby notes that no variable called body exists, so invokes the method_missing method, and everything is good. Not so with title = 'My new title'. Ruby assumes that this is an assignment to a new variable, rather than a method invocation (why does the interpretor not check in the method exists first? I can only assume this is a performance issue). This has lead to some obscure bugs where values are mysteriously failing to get assigned. The solution is to prefix with self., which gives Ruby the hint that this is a method call.
class Post < ActiveRecord::Base
def test
self.title = 'My new title'
s = body
end
end


Migrations
One of the most common reason my unit tests do not work is that I have done a migration on the development database, and forgotten the testing database (rake db:test:prepare).

Fixtures and Migrations
When you create a model, some default fixtures are also created for testing. If you subsequently modify the migration file, those fixtures might not work, and you will get an error like this when you run your test:
ActiveRecord::StatementInvalid: Mysql::Error: #42S22Unknown column


Reverting to Rails 2.2.2
Rails 2.3.2 adds a new helper file for new models, in units/test/helpers. If you then revert to 2.2.2, this will crash rake when you run your unit tests (but other tests will be okay).

Saving records
When you save a record (and it is not a new record), Rails will work out what has changed, and only update those fields that have actually changed. However, it is not that reliable at spotting a change. In this example, no change is made.
r = MyRecord.find 19
r.description.sub! 'this', 'that'
r.save

The problem is that Rails uses a flag on each attribute, and if you do not set the flag, the attribute is not updated. Assignment automatically sets the flag, so it is easy to over-look. The following will work fine:
r = MyRecord.find 19
r.description = r.description.sub 'this', 'that'
r.save

The alternative is to tell Rails explicitly that the attribute is being changed:
r = MyRecord.find 19
r.description_will_change
r.description.sub! 'this', 'that'
r.save
Refs:
http://api.rubyonrails.org/classes/ActiveRecord/Dirty.html
http://ryandaigle.com/articles/2008/3/31/what-s-new-in-edge-rails-dirty-objects

First may not be first
I have database that has been in use for some ten months ago. I wanted to retrieve the very first sample. Should be easy:
Sample.find(:first)

Apparently not. This was retrieving a record from only a couple of weeks ago, with an ID of 1269. And in my development database, when I tested this, it worked fine. To get the right sample, I had to specify what to order by (I have no idea what ordering brings record 1269 to the front).
Sample.find(:first, :order => "created_at ASC")



Erroneous commas in hash assignments
This one is not limited to models. If you are assigning values to a hash, an extra comma can wreak havoc! Usually extraneous punction is ignored (extra semi-colons, for example), or throws an error. Not in this case.
h = {}
h[:name] = 'Fred'
h[:desc] = 'Big'

p h.inspect
# => "{:name=>\"Fred\", :desc=>\"Big\"}"

h[:name] = 'Fred',
h[:desc] = 'Big'

p h.inspect
# => "{:name=>[\"Fred\", \"Big\"], :desc=>\"Big\"}"


Changes to a file not always noted
While you are developoing your system, chances are you will have your local web server running, and will be looking at how the web site behaves and looks as you make changes. Generally, this is fine. However, some changes that you make are not going to change the web site, for example, changes to files in config. If you have the console open, and type reload! you might find that even changes to your model (I think this may be when connected to the production database, so it is going to be very rare, admittedly). You may need to shut down the console or web server, and restart it to get the changes to have an effect.


Struggling with Ruby: Contents Page