ActiveRecord Mass-Assignment Order

For over a month, I received occasional reports that an existing feature, adding secondary accounts, wasn't working. I could never reproduce the failure in any browser or with a unit test, though.

Recently, I received a report about another feature, creating an account, that failed. I could see it fail if I watched the application log while the feature was used, but, again, I couldn't reproduce the failure.

Calling for Reinforcements

I revisited the problem this week. After many more attempts at reproducing the problem, I was out of ideas. I called for reinforcements. I needed another pair of eyes on this one.

It wasn't long before there was a spark! My pair remembered facing a problem on an old project. It involved mass-assignment. The feature on my project used mass-assignment. It involved overwritten mutators. The feature on my project used overwritten mutators. Could this problem be the same?

The Overwritten Mutators

Here are the overwritten mutators, password= and password_confirmation=.

 1 class Account < ActiveRecord::Base
 2   validates_confirmation_of :password
 3 
 4   def password=(value)
 5     if store_locally?
 6       write_attribute(:password, encrypt(value))
 7     else
 8       ldap_account.password = value
 9     end
10   end
11 
12   def password_confirmation=(value)
13     @password_confirmation = if store_locally?
14       encrypt(value)
15     else
16       ldap_account.password_confirmation = value
17     end
18   end
19 end

They either store their value locally or on an object backed by LDAP. This decision is made in store_locally?, which checks another attribute named role_type.

The Problem

Here's the code that has been intermittently failing.

1 def create_login_account(dealer)
2   Account.create!(
3     :role_type             => AccountRole.dealer,
4     :user_name             => dealer.user_name,
5     :password              => dealer.password,
6     :password_confirmation => dealer.password
7 )
8 end

It would cause the password confirmation validation to fail, complaining that the provided password and password confirmation didn't match.

The Solution

password= and password_confirmation=, through store_locally?, depend on role_type. Though create_login_account builds the hash of attributes in an order which puts role_type first, there is no guarantee 1 on the order in which the attributes will be used to assign the attributes on Account. Thus, password or password_confirmation could be assigned before role_type.

To ensure that role_type is assigned before password and password_confirmation, we abandon mass-assignment, instead assigning the attributes one at a time.

1 def create_login_account(dealer)
2   account                        = dealer.build_account
3   account.role_type              = AccountRole.dealer
4   account.user_name              = dealer.user_name
5   account.password               = dealer.password
6   account.password_confirmation  = dealer.password
7   account.save!
8   account
9 end

This approach, though not necessarily idiomatic, solves the problem. I added a brief comment in the project so that anyone, including me, who works on this code later will know why it deviates from the norm.

Notes

  1. There's no guarantee in Ruby 1.8, anyway, which this application uses. In Ruby 1.8, “The order in which you traverse a hash by either key or value may seem arbitrary, and will generally not be in the insertion order.” In Ruby 1.9 and 2.0, “Hashes enumerate their values in the order that the corresponding keys were inserted.”
Craig Demyanovich, Software Craftsman

Craig Demyanovich is an avid hockey player, and loves visiting new places with his wife, Sandy.

Interested in 8th Light's services? Let's talk.

Contact Us
+ + +