Validating Associations in Rails
Validating our Rails associations can be a balancing act. Insufficient validation can lead to garbage in the database. Overzealous validation can lead to needless complexity. Here are a few lessons I’ve learned:
-
Validate presence_of where there is a belongs_to. Consider the crazy cat lady:
class CrazyCatLady < ActiveRecord::Base has_many :cats, inverse_of: :crazy_cat_lady end class Cat < ActiveRecord::Base belongs_to :crazy_cat_lady, inverse_of: :cats endShould a
catexist independently of acrazy cat lady? Philosophical ramifications aside, we’ll say no. Since we only really care about tracking crazy cat ladies, the cats are only important inasmuch as they relate to their ladies. Thebelongs_tois a guide that we probably want to validatepresence_of, like so:class CrazyCatLady < ActiveRecord::Base has_many :cats, inverse_of: :crazy_cat_lady end class Cat < ActiveRecord::Base belongs_to :crazy_cat_lady, inverse_of: :cats validates_presence_of :crazy_cat_lady endWhy aren’t we also validating
presence_ofthe foreign key (crazy_cat_lady_id)? We’ll see in a moment. -
Use not null constraints for foreign keys. Since we’re validating presence of the
crazy_cat_ladyon acat, we should also make sure that a foreign key is present on thecatdatabase entry. In this case, the foreign key is just the primary key (theid) of acrazy_cat_ladythat is stored with acat, to let Rails know where the association is stored.
We could do this in theCatmodel:validates_presence_of :crazy_cat_lady_idBut this is going to make our development more difficult later on. Let’s say we want to create a new
crazy_cat_ladyand give her acatall in one database transaction, like so:new_lady = CrazyCatLady.new new_cat = new_lady.cats.new new_cat.save!This would raise an error! Since we’re validating presence of the
crazy_cat_ladyforeign key in the model, and thecrazy_cat_ladyhasn’t yet been saved (which would generate theid), the validation fails. Instead of validating presence of the foreign key in the model, let’s add a database constraint:class CreateCats < ActiveRecord::Migration def change create_table :cats do |t| t.string :color t.integer :crazy_cat_lady_id, null: false t.timestamps end end endNotice the
null: falsein our migration. This enforces our intent to prohibit null values for the foreign key on the database layer of the Rails stack. A nil foreign key will now pass model validation, but it will raise an exception if it is saved to the DB. Effecting this change, when we again issue:new_lady = CrazyCatLady.new new_cat = new_lady.cats.new new_cat.save!It works! Rails has done something a bit clever here, saving the
new_ladyto the database automatically, before saving thenew_cat. Once thenew_ladyis saved and has a primary key, it uses that for thenew_cat‘s foreign key. Neat! -
Optionally, use foreign key constraints. This is a bit more advanced. SQL databases like PostgreSQL and MySQL allow you to specify foreign key constraints. The database will make sure that any foreign key with this constraint matches to a corresponding primary key in the table you specify. Doing this helps to maintain referential integrity; it makes it less likely that our database will have foreign keys pointing to wrong entries, or worse, non-existant entries.
I use Foreigner, a gem that nicely simplifies the process. After including it in our gemfile and running
bundle install, we could add the following line to ourCatmigration:t.foreign_key :crazy_cat_ladiesHere, we’re just specifying the name of the table that holds the primary key of our association. Now, when we run the migration, Foreigner will instruct our database to apply the corresponding foreign key constraints. That’s all there is to it!