2008/06/07

Using non-standard primary keys with ActiveRecord in Ruby on Rails

A Rails application I'm currently working on has a 'User' model. For the purposes of this app. a 'user' is uniquely identified by their mobile phone number - a user must have a mobile phone number, and may only have a single one. A second mobile phone number must be a second user.

I'm going to need the mobile phone number in other models - e.g. "User has_many :messages", and I'd like to have a "mobile_phone_number" attribute in my Message model, so that I can say "message.mobile_phone_number" without having to join the users table all the time, like this; "message.user.mobile_phone_number".

I can get what I want if the mobile_phone_number is the primary key of the User model. Let's try it.

Here is the migration (this is specific to mysql);



class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users, :id => false do |t|
t.integer :mobile_phone_number
end
execute "alter table users modify column mobile_phone_number bigint unsigned primary key"
end

def self.down
drop_table :users
end
end



(The alter table statement is a bit of a hack. Another way to achieve the same result would be to use the mysql_bigint gem)

Here is the model;



class User < ActiveRecord::Base
set_primary_key :mobile_phone_number
end



Looks OK, but this is what happens when you try to create a User;



>> u = User.create :mobile_phone_number => 447123456789
=> #<User mobile_phone_number: 0>



Where did that '0' come from, and what happened to the mobile_phone_number?

A quick look in the logs shows this;



WARNING: Can't mass-assign these protected attributes: mobile_phone_number
SQL (0.000146) BEGIN
User Create (0.000222) INSERT INTO `users` VALUES(DEFAULT)
SQL (0.000422) COMMIT



So, Rails is not sending the primary key column value in the SQL create statement. This would make sense if, as usual, our primary key was an autoincrement column. The database would supply that value for us, so we don't want to send it. But, in our case, we want to set the primary key ourselves, so this behaviour is wrong. (The 0 comes from the default value of the column)

You can try using 'attr_accessible' to allow you to assign to the mobile_phone_number, but it doesn't work (presumably because it's the primary key that we're trying to assign to).

We need some way to override the standard Rails primary key behaviour so it does what we want. That sounds a lot like the composite_primary_keys gem.

So, after installing the gem (in my case, after installing Rick Olson's 'gems' plugin so that I can put the composite_primary_keys gem in my project's vendor directory), here is the updated model;



require 'composite_primary_keys'
class User < ActiveRecord::Base
set_primary_keys :mobile_phone_number
end



Note that 'set_primary_key' has changed to 'set_primary_keys'.
Now, we get this;



>> u = User.create :mobile_phone_number => 447123456789
=> #<User mobile_phone_number: 447123456789>



Much better. Even though we're not using a composite_primary_key, the gem is overriding the default Rails behaviour so that the key we want is being set by the create statement.

If you know a more elegant way to do this, please let me know.