The Magic of has_many associations in Rails 4

If you’ve recently switched to Rails 4 or are just starting out learning to use Rails, then you may have come across some weirdness when trying to set a has_many association for real in one of your forms.

You see, in Rails 4, we got some improvements related to the security of our forms, in particular what parameters we’ll accept from a user submitted form. In previous versions of Rails, it was up to the developer building on top of the framework to implement this, usually using a mix of attr_accessible and slice on the params hash.

Then, there were some high profile security breaches related to the fact that developers weren’t actually doing this. And it was decided it was time to pull a solution for this into the core Rails project.

The Rails 4 solution is to use parameter whitelisting by default.

The downside of this means that lots of beginners don’t know about this; after all, when you’re learning Rails, there is a lot to cover and it can take a while to get to some of the less sexy aspects like security.

There isn’t a huge amount of documentation for this either, at least not for Rails 4. This means that people searching for info on this tend to get an out of date fix relating to Rails 3, which doesn’t work and leaves you feeling frustrated.

It makes sense for the framework to provide a sensible default that helps beef up the security of your app for you, without requiring much initial knowledge or time from the developer.

The documentation for how to use this seems buried within the Rails guides and the error messages you get are quite subtle.

This presents us with an opportunity to get familiar with a good pattern for tracking down bugs in our code. In this case, related to incorrect use of the framework.

A has_many association in a form

Suppose we are trying to model the following system for managing surveys. We have a survey and we have a user. A user can be invited to multiple surveys. So we start with code something like this:

class User < ActiveRecord::Base
  has_many :invites
  has_many :surveys, through: :invites
end

class Invite < ActiveRecord::Base
  belongs_to :user
  belongs_to :survey
end

class Survey < ActiveRecord::Base
  has_many :invites
  has_many :users, through: :invites
end

class SurveysController < ApplicationController
  respond_to :html

  def create
    @survey = Survey.create params(:user_ids)
    respond_with @survey, location: [:surveys]
  end
end

The problem with this code is that we haven’t provided any whitelist for the correct field, in this case user_ids.

Rails doesn’t raise an obvious error for this, like it would with other incorrect uses of the framework. Instead you will get a subtle error message appearing in the app logs, along the lines of Unpermitted params :user_ids.

It’s easy for this to go by unnoticed because the form will submit but the values will never get passed to your database. Usually when you have a bug in your code, it causes Rails to raise an exception or you get some kind of validation error on your object, however in this case you have to go digging to find out what happened. The beginning Rails developer can end up in a state of confusion, wondering why the value won’t save yet there’s no apparent error message.

Check your logs

Bear with me on this slight detour but this gives us an opportunity to talk about a useful debugging technique. Your application logs.

Take a look in log/development.log and you will see a lot of output. On Mac OS X you can fire up the console app to watch this file update live as you make requests in your app. Alternatively, in your terminal just type tail -f log/development.log and watch the output there.

What are we looking at? In this particular case, we performed an action in our app that didn’t have the expected result, so that’s where we start looking in our logs.

    Started POST "/surveys" for 127.0.0.1 at 2014-01-09 17:24:19 +0000
    Processing by SurveysController#create as HTML
        Parameters: {"utf8"=>"✓", "authenticity_token"=>"OCK399rbR0tkFLgO+7XJa9hbyJ9gKewT+aGxZR6qP0c=", 
        "survey"=>{"name"=>"test", "user_ids"=>["", "1", "2", "3"]}, "commit"=>"Create Survey"}
    Unpermitted params :name, :user_ids

Look for the action you just called and see if you find anything interesting. In our case, it’s the POST /surveys line we want and when we find it we discover an error message waiting for us.

So, what is the correct way to do this?

Every form field that you wish to save must be marked as safe to pass through to your database. Rails gives us a couple of methods on the params hash to do this.

Firstly, permit accepts a list of safe parameter names we are happy to pass through from the form.

Secondly, require allows us to raise an error where a parameter isn’t present.

In your controller, you can do this.

class SurveysController
  ...

  private

  def survey_params
    params[:survey].permit(:name, :user_ids)
  end
end

This would allow the name form field to be passed through to our database. This is progress but will still fail silently if you try to pass a list of ids like you would for a collection. This special case requires a slight change in our list of accepted parameters.

class SurveysController
  ...

  def survey_params
    params[:survey].permit(:name, user_ids: [])
  end
end

Now we’re in business. When passing an array from our form, we need to inform the whitelister that this is what we intended, otherwise it will only let through one of the following types String, Symbol, NilClass, Numeric, TrueClass, FalseClass, Date, Time, DateTime, StringIO, IO.

Check out the code for this Rails 4 has_many example on Github.

Don’t miss the next post on using Rails like a pro

Sign up to receive my regular newsletter and get tips and advice to help you go beyond the tutorials and really start using Rails to its full potential.