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
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.
Consider as an example, an application in which the
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
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
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.
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
ActiveRecordclasses and allow them to focus on their persistence responsibility (e.g. hash and salt the password before passing assigning it to
- 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.