How to Stop Using Nested Forms
One issue that caused me a lot of pain on my first few rails projects was the natural coupling that developed between the database and the rest of my application. The “skinny controller, fat model” mantra has been the prevalent in the Rails community since the early days. The problem with this philosophy is that it is only 50% accurate. If you are building good object oriented software, you shouldn’t have a fat anything. When all of your business logic is encased in ActiveRecord objects, there can be some unfortunate consequences: things become difficult to reason about, it is hard to test objects in isolation and changing the database schema is a painful process that demands updates to many parts of the application.
In my opinion, one of worst offending features of rails is the ability to build nested model forms with fields_for
and accepts_nested_attributes_for
, as doing so directly couples your view layer to your database schema. Lately, I have been using a very simple technique to prevent this problem that I call building aggregate models.
An Example
Consider as an example, an application in which the User
model has_one
associated Email
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
The rails way to handle this association is to build a nested model form via fields_for
.
1 2 3 4 5 6 7 8 |
|
The obvious problem with this is that it couples the view directly to the database structure. If we decided to make changes to the database schema later, the form will need to be updated. I also find that accepts_nested_attributes_for
is awkward to test and the subtleties of the api are difficult to remember and work with (e.g. mass assignment errors, associated validations).
Another option that many rails developers might opt for in this situation is to de-normalize the database and smash the emails
and users
tables together into one. In this case I decided to keep emails
as a separate entity because they are going to have their own attributes (e.g. verified?
). I also anticipate a requirement that users will have many emails. While there is a case to be made against normalization in some situations, the fact that it makes your view layer simpler to code is part of it.
The Solution
Lately, the approach I have been using in these situations has been to create a class to accept the form data and translate it to the active record layer. In Rails 3.0 the API required by controllers and views was extracted into a set of modules that can be included as needed. This allows us to create an object that is guaranteed to jive with form_for
(or any other form gem you may be using) that is completely decoupled from ActiveRecord
. To handle the example above, we might end up with something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
1 2 3 4 5 6 7 8 9 10 |
|
By adding a thin layer of indirection, this pattern reduces the coupling between the view layer and database. There are a few other big wins that come with it as well:
- the form markup is now as simple as it would be for one model with no associations
- we can move business logic out of the
ActiveRecord
classes and allow them to focus on their persistence responsibility (e.g. hash and salt the password before passing assigning it toUser
) - we can add a different set of validations at the profile level (e.g. that are only pertinent to new users for example the confirmation of password)
Obviously we could enhance the Profile
class to make it feel more like an ActiveRecord
object (e.g. define update_attributes
or a static find_by_user_id
method that initializes the model for existing records) but for simple cases there is no need.
As always, this pattern should be used sparingly. Resist the urge to optimize prematurely.