Friday, November 18, 2011

Rails: Adding a 'reference' migration

So yes, I did jump on the Ruby On Rails Bandwagon as it was making it's fifth round of town. I started doing Ruby earlier, when I became focused on DevOps and it turns out a lot of the tooling around DevOps is written in Ruby.

And Ruby is a nice language. I use it for everything for which I would have used Perl earlier, but also for a lot more serious programming than that. Earlier this year, the RedDotRubyConf came to Singapore, where I'm living now. That was a great opportunity to catch up on what's going on around Ruby, with all the big names from the Ruby world making their appearances as speakers.
I even made second place in the Ruby programming competition.

So now I'm looking at Rails more seriously. I worked a lot with Java frameworks like Wicket in the past. I still like the component-based approach, and I'm missing that in Rails. But it's absolutely easier to develop web-sites in Rails faster, and they are still structured enough to be maintainable as well, unlike many other page-based web development frameworks.

As I am trying to develop a site for myself, I ran into some issues with migrations. It's a site to display some holidays pictures in a way that I can't do with any existing apps that I know of, and it's a nice exercise anyway. I have my product owner who tells me what features she likes in it and I program them :-)

The issue occurs when I create a migration to add a 'references'-type column to a model. I forgot to add a 'main photo' field to my Photoset model, so I'm adding that with a migration:

rails generate migration add_main_photo_to_photoset main_photo:references

The migration looks like this:

class AddMainPhotoToPhotoset < ActiveRecord::Migration
  def change
    add_column :photosets, :main_photo, :references
  end
end


Now when I try to run that, I get an error:


$ rake db:migrate
==  AddMainPhotoToPhotoset: migrating =========================================
-- add_column(:photosets, :main_photo, :references)
rake aborted!
An error has occurred, this and all later migrations canceled:

SQLite3::SQLException: near "references": syntax error: ALTER TABLE "photosets" ADD "main_photo" references

Tasks: TOP => db:migrate
(See full trace by running task with --trace)

That's because it is trying to literally create a column named 'main_photo' with the SQL type 'references' - which doesn't exist. What I want it to create is a column called 'main_photo_id' with the type 'integer'.
I could change the migration to do that, and it would work. But wait - when I create a model initially, I am free to use the type 'references' - so why can't I do that when I add a new column later?


When I created the Photoset model, the migration looked like this:


$ rails generate model photoset name:string region:references


class CreatePhotosets < ActiveRecord::Migration
  def change
    create_table :photosets do |t|
      t.string :name
      t.references :region

      t.timestamps
    end
    add_index :photosets, :region_id
  end
end


A photoset belongs to a region of the world. This works.

So if I change the migration that adds a main_photo column to this, it also works:


class AddMainPhotoToPhotoset < ActiveRecord::Migration
  def change
    change_table :photosets do |t|
      t.references :main_photo
    end
  end
end



That looks nice and clean and it looks just like the migration that created the model. There is value in consistency.

So now I know how to fix this. I'm only left wondering - am I missing something? Why didn't wasn't the migration created like this in the first place?
If this is something that still needs to be updated in the rails code, that's fine. I don't feel ready yet to dive into the framework's code and contribute to it, but I can do this by hand right now.

As long as there is no underlying reason why this change_table approach is a bad idea, that is.