Wednesday, September 2, 2009

Foreigner comes full circle


After actively developing multiple Rails projects over the last three years, I've learned when to not fight the framework, and I've also learned when it's appropriate to roll up your sleeves and write some Ruby code. I've stopped using composite primary keys, I've stopped attempting to namespace models, I always use the default "id" for a primary key, the list goes on.

One thing my mind has not changed on is using foreign keys. I use them, and I've finally completed Foreigner, a foreign key migration library that meets my initial requirements:
  • Do not change existing behavior of the application
  • Perform a no-op when the adapter does not support foreign keys
  • Follow similar API as 'add_index'
  • Dump foreign keys into schema.rb


Do not change existing behavior of the application

I found numerous existing foreign key plugins that attempted to automagically add foreign keys. For example, automatically adding foreign keys based on column names, or automatically adding associations to active record models base on foreign keys in the database. I believe both of these are mistakes. Automatically adding foreign keys changes existing migrations, and always seems to break things. Automatically creating active record associations puts too much guesswork into play. There's a difference between using foreign keys to enforce integrity (good), and using foreign keys to change the behavior of the application (bad).

Lastly, I thought long and hard about whether calls to t.references should automatically add a foreign key. I finally resolved on not doing this, and compromised by adding a new :foreign_key option. For example:
change_table :comments do |t|
t.references :author, :foreign_key => true
end

Perform a no-op when the adapter does not support foreign keys

While this does not help me, it is required to support adapters such as sqlite3. The AbstractAdapter implements all necessary methods with a no-op. With Foreigner installed, you can develop with sqlite3 and run production with MySql.


Follow similar API as 'add_index'

Adding foreign keys isn't much different than adding indexes, and everyone is familiar with using add_index. You add_foreign_key, remove_foreign_key and can even use change_table.


Dump foreign keys into schema.rb

The final feature I added to Foreigner is dumping foreign keys to schema.rb, so that you can avoid using the SQL structure. Similar to how indexes are added to schema.rb, Foreigner inspects the database and determines the foreign keys.


In conclusion...

There's been a lot of discussion in the past about whether or not you should be using foreign keys. This shows to me is that there is a large group of people who probably want this available out of the box with Rails. The library was designed to not affect those who don't want them; the primary purpose in doing so was to show that everyone can have it their own way. My next step is turning this into a gem.

Get the source: http://github.com/matthuhiggins/foreigner/tree/master.