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.
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.
If you submit the form with updates values in
ModelA, the values in
ModelB are updated using the old version of
before_validate callback method we were attempting to get the
ModelA attribute foo using the
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.
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.