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 end
Should a
cat
exist 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_to
is 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 end
Why aren’t we also validating
presence_of
the 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_lady
on acat
, we should also make sure that a foreign key is present on thecat
database entry. In this case, the foreign key is just the primary key (theid
) of acrazy_cat_lady
that is stored with acat
, to let Rails know where the association is stored.
We could do this in theCat
model:validates_presence_of :crazy_cat_lady_id
But this is going to make our development more difficult later on. Let’s say we want to create a new
crazy_cat_lady
and give her acat
all 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_lady
foreign key in the model, and thecrazy_cat_lady
hasn’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 end
Notice the
null: false
in 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_lady
to the database automatically, before saving thenew_cat
. Once thenew_lady
is 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 ourCat
migration:t.foreign_key :crazy_cat_ladies
Here, 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!