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.