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 a crazy 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. The belongs_to is a guide that we probably want to validate presence_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 a cat, we should also make sure that a foreign key is present on the cat database entry. In this case, the foreign key is just the primary key (the id) of a crazy_cat_lady that is stored with a cat, to let Rails know where the association is stored.
    We could do this in the Cat 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 a cat 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 the crazy_cat_lady hasn’t yet been saved (which would generate the id), 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 the new_cat. Once the new_lady is saved and has a primary key, it uses that for the new_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 our Cat 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!

comments powered by Disqus