I am a Rails Developer and I believe in data integrity. There I said it! And look, there are lots of people in the club. Jer on Rails had an awesome post in support for foreign keys a while back.
After my talk on supercharging ActiveRecord to behave in an enterprise environment at the MySQL conference, a lot of folks were interested in how to get around the Rails ActiveRecord errors that appear when one starts deifying the Rails Way and uses foreign keys and unique indexes on the database. Well I thought about it and came up with not only a list but a yet another new plugin rails_devs_for_data_integrity to help deal with some of the errors. So when someone tells you not to use foreign keys, don’t listen and join the Rails Devs for Data Integrity! Here is how to have em, and still make pretty Rails apps.
validates_uniqueness_of introduces a query to check unique records before insert or update, but alone doesn’t guaranty uniqueness in a multi server/multi mongrel environment. By using it conjunction with a unique key on the database, you have proper rails error messages and your safety belt to retain data integrity. In most cases when dealing with a single instance (a single user’s user name for example), I can afford to eat the overhead of the rails validates_uniqueness_of query.
Use on duplicate key update and ignore
ar-extensions 0.9.1 supports ON DUPLICATE KEY UPDATE and IGNORE for MySQL on import, save, and create (all inserts and updates). This is especially practical for import (bulk insert) where validating each record’s uniqueness would produce a lot of overhead.
Catch and Handle the Duplicate Violation Exception
I wrote a lot of custom code in models and controllers to catch MySQL duplicate key error (ActiveRecord::StatementInvalid) and handle them appropriately. Typically, I added a new error to ActiveRecord::Base.errors, which is then displayed nicely with error_messages_for in the view. Sometimes this was done often with a save_safe model method possibly aliased to save or a rescue on the controller action.
After MySQLConf I decided to write a little plugin rails_devs_for_data_integrity to convert exceptions into ActiveRecord errors (like validations do) for tables with unique and foreign keys . Its still infantile (and without tests, gasp!) but if anyone likes to find bugs, hit me up on githubs new issue tracker. If there is more than one unique key per table, you might want write some custom error handling methods.
script/plugin install git://github.com/blythedunham/rails_devs_for_data_integrity.git
class User < ActiveRecord::Base handle_unique_key_violation :user_name, :message => 'is taken" handle_foreign_key_violation :primary_email_id, :message => 'is not available' end
Will write ActiveRecord errors instead of nasty MySQL errors:
>> user.errors.on(:user_name) => "association does not exist." >> user.errors.on(:primary_email_id) => "is a duplicate."
Overwrite rescue_action_in_public in ApplicationController
Its a good idea to either send the user to a pretty 404 static generic error page (fast) or write some custom code in rescue_action_in_public. One idea is to create DisplayableException and subclass any exceptions where the text can be displayed to the user. If this is thrown, show exeception.to_s, if not show a generic error.
def rescues_action_in_public(exception) @message = if exception.is_a?(DisplayableException) exception.to_s else "Sorry but an error occurred. Please contact your mommy." end render :action => 'error_page' end
I have been using the Redhills foreign key migration plugin for a long time and haven’t had too much trouble with foreign key violations.
Trouble Deleting Records
At one point ON DELETE was not specified to SET NULL or CASCADE on many dependent columns and there was trouble deleting rows. Reindexing the database with this option was the solution. Similarly, one could use ON UPDATE on foreign key indexes. However, due to the way ActiveRecord works and doesn’t really update id columns, I haven’t had any issues.
Foreign Key Validation Errors
This has happened so seldom that I never worried about it. That’s what the refresh button is for! Perhaps its because most users have their own data and aren’t really modifying and deleting simultaneously. However, sure this can happen and it throws an ActiveRecord::StatementInvalid exception as with unique key violations. I would recommend the same approach as for unique keys: catch and handle the exception. I added foreign key support to the rails_devs_for_data_integrity plugin experiment. Yay!