Showing posts with label composite_primary_keys. Show all posts
Showing posts with label composite_primary_keys. Show all posts

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.

2008/04/26

Gotcha: composite_primary_keys gem

Dr. Nic's composite_primary_keys gem is incredibly helpful if you're writing rails code against a legacy database, or in any other situation where you can't or won't follow the rails convention of having a single field as your model's primary key.
After doing the usual;

sudo gem install composite_primary_keys
...and requiring it in you environment.rb file, you can define a model like this;
class Membership < ActiveRecord::Base
set_primary_keys :user_id, :group_id
...
end
Brilliant.
But, there is one subtlety you need to be aware of. If your model mixes in any modules, you might end up writing something like this;
class Membership < ActiveRecord::Base
include MyAwesomeModule
set_primary_keys :user_id, :group_id
...
end
Looks fine, doesn't it? Unfortunately, your tests will now break with lots of errors like this;
ActiveRecord::StatementInvalid in 'Membership should foobar'
Mysql::Error: Column count doesn't match value count at row 1: INSERT INTO memberships (`user_id`, `group_id`, ... , id) VALUES (...whatever...)
/Users/david/myproj/vendor/composite_primary_keys-0.9.90/lib/composite_primary_keys/base.rb:106:in `create_without_callbacks'
See that last id, just before VALUES? It shouldn't be there. Something weird is going on, because composite_primary_keys doesn't seem to be doing its thing.
The solution is to make sure the call to "set_primary_keys" is the first thing executed in your model;
class Membership < ActiveRecord::Base
set_primary_keys :user_id, :group_id
include MyAwesomeModule
...
end
Remember this, and everything works fine. Forget it, and you'll have lots of fun with the debugger.
If I were braver, smarter and kinder, I would dive headfirst into the code and try to fix it. But, I've got work to do.