Skip to content

Ruby on Rails gotcha: updating child records with callbacks and nested attributes

I recently ran into a bit of a gotcha concerning the way nested records get updated in Rails, which in hindsight makes total sense, but caused some confusion at the time.

The setup

ModelA has_many ModelB‘s

ModelB belongs_to ModelA

ModelA accepts_nested_attributes_for ModelB

ModelB has attributes that is derived in part from values held in ModelA, the username and password, the client has some slightly unusual auth requirements involving a “group” password and scoped logins, which has required some serious bending of Devise.  These attributes are updated using a before_validation callback, because they must be present and correct for validation to succeed and for the model to save.

We had a form that included all the fields for ModelA and used cocoon for managing the collection of ModelB‘s.  This form submits to the ModelA update controller method.

The Problem

If you submit the form with updates values in ModelA, the values in ModelB are updated using the old version of ModelA.

The cause

In the ModelB before_validate callback method we were attempting to get the ModelA attribute foo using the belongs_to relationship.

However, the updates to the ModelA object are not committed to the database until all of the objects are updated and the transaction finishes. This means that when we call model_b.foo, ActiveRecord will fetch the old instance of ModelB from the database, rather than referring to the updated version.

Solutions

There are several fixes for this.  It actually turned out that from a user perspective it made more sense to split out the ModelA form from the ModelB collection form, which made the problem go away.  But that’s not actually an ideal in some situations.

If that isn’t an option the alternative is to use a different callback, and move the update logic to the ModelA, so the updates are pushed, rather than pulled from ModelB. The update could be done from any number of points, but one option is the new after_commit callback, introduced in Rails 3.  However this would only work if your ModelB attributes can be left as nil until after everything has been created.   In our case that wasn’t possible, these were Devise user models, so the username and password can’t be nil.

The final option is to break out the updates in the controller, so the ModelA gets updated first, then the children get updated/created.  This is equivalent to automating the first option.

None of the solutions is perfect, so if anyone can recommend a better way I’d love to hear about them.