If you find yourself getting validation errors when using accepts_nested_attributes_for
with has-many-through relations, the answer may be to add an inverse_of
option.
The inverse_of
option allows you to tell Rails when two model relations describe the same relationship, but from opposite directions. For example, if a User has_many :posts
and a Post belongs_to :user
, you can tell Rails that the :user
relation on Post
is the inverse of the :posts
relation on User
.
This option is usually not required, but there are cases where it matters. One such case is when using accepts_nested_attributes_for
with a has-many-through relation. This will eventually lead to a collection=
assignment which is only possible if Rails knows that one relation is the inverse of another.
Example
In our case, we had the following three models:
class Notice < ActiveRecord::Base
# attribute :title
has_many :entity_roles
has_many :entities, through: :entity_roles
accepts_nested_attributes_for :entity_roles
end
class EntityRole < ActiveRecord::Base
# attribute :name
belongs_to :entity
belongs_to :notice
validates_presence_of :entity
validates_presence_of :notice
accepts_nested_attributes_for :entity
end
class Entity < ActiveRecord::Base
# attribute :name
# attribute :address
has_many :entity_roles
has_many :notices, through: :entity_roles
end
We wanted the form to create two related entities for the notice, each of a specific role.
The controller looks like this:
class NoticesController < ApplicationController
def new
@notice = Notice.new
@notice.entity_roles.build(name: 'submitter').build_entity
@notice.entity_roles.build(name: 'recipient').build_entity
end
end
Using Simple Form, the view looks like this:
<%= simple_form_for(@notice) do |form| %>
<%= form.input :title %>
<%= form.simple_fields_for(:entity_roles) do |roles_form| %>
<% role = roles_form.object.name.titleize %>
<%= roles_form.input :name, as: :hidden %>
<%= roles_form.simple_fields_for(:entity) do |entity_form| %>
<%= entity_form.input :name, label: "#{role} Name" %>
<%= entity_form.input :address, label: "#{role} Address" %>
<% end %>
<% end %>
<%= form.submit "Submit" %>
<% end %>
The only clever bit here is that we use each role’s name to
intelligently affect the entity form’s labels each time it’s rendered.
Aside from that, it’s pretty standard accepts_nested_attributes
stuff.
On POST, we found validation errors on the entity_role
objects:
["notice", "can't be blank"]
We were confused.
The controller’s #create
action is effectively doing this:
notice = Notice.new(
title: "...",
entity_roles_attributes: [
{ name: "submitter", entity_attributes: { ... } },
{ name: "recipient", entity_attributes: { ... } }
]
)
notice.save
Which, as far as we knew, should work.
It seemed Rails was not setting the notice
attribute on the
EntityRole
before attempting to save it, triggering the validation
errors. This is a bit surprising as other has_many
relations (omitted in this blog post) should have the same save mechanics and were working just fine.
In an act of experimentation, we added inverse_of
:
class Notice < ActiveRecordBase
has_many :entity_roles, inverse_of: :notice
end
And suddenly, it all worked.
Only after the fact, when we knew to include “inverse_of” in our search queries, did we find some information on this issue. You can read the details here, here, and here if you're interested.
When you use collection=
assignment with a has-many-through (as accepts_nested_attributes_for
does), you have to specify inverse_of
for Rails to save everything correctly.