Blog by Railsware

Rails 3.2.9 through associations issues

Recent 3.2.9 release bugfix has brought an issue for through associations.
inverse_of usage can solve the problem only partially (polymorphic associations are still affected).

UPDATE: patch that introduced this issue has just been reverted in rails master.

Few days ago Rails 3.2.9 was released.
Among the bugfixes a fix for through associations is present, and since 3.2.9 join model instances are built even though the object is not saved yet.

To get the point take a look at these simple models with User and Team joined with each other through UserMembership:

class User < ActiveRecord::Base
  has_many :user_memberships
  has_many :teams, through: :user_memberships
end

class Team < ActiveRecord::Base
  has_many :user_memberships
  has_many :users, through: :user_memberships
end

class UserMembership < ActiveRecord::Base
  belongs_to :team
  belongs_to :user
  validates :team, :user, presence: true
end

Before Rails 3.2.9 this code does not build any UserMembership instances:

team = Team.new(users: [User.first])
team.valid?           #=> true
team.user_memberships #=> []

But since Rails 3.2.9 UserMembership instance is built:

team = Team.new(users: [User.first])
team.valid?                      #=> false
team.user_memberships            #=> [#]
team.user_memberships.first.team #=> nil

Oh, have you noticed that team is invalid now? Aahh, now when it get’s complicated.

Built instance of a join UserMembership model
has no team set and that’s why it’s invalid (it has validate :team, :user, presence: true).
As soon as join model instance is invalid team is invalid as well.

To fix this validation issues in this particular case the one should use inverse_of for the associations like this:

class User < ActiveRecord::Base
  has_many :user_memberships, :inverse_of => :user
  has_many :teams, through: :user_memberships
end

class Team < ActiveRecord::Base
  has_many :user_memberships, :inverse_of => :team
  has_many :users, through: :user_memberships
end

team = Team.new(users: [User.first])
team.valid?                        #=> true
team.user_memberships.first.valid? #=> true
team.user_memberships.first.team   #=> #

inverse_of takes care of associations objects in-memory synchronization, so now UserMembership instance has a team and it’s valid.
Unfortunately polymorphic associations are still affected – follow this github discussion for more info.

That said, if you’ve just upgraded to 3.2.9 and your tests are failing review the associations and fix them whenever it’s possible by using inverse_of.

Exit mobile version