{ Parking for coders only }

Creating records via a has_many :through association

Today I found a really odd situation while working on a Rails 4 project. I created a couple of data models having a many-to-many association with has_many :through in both directions. A bit of code will help clearing up things:

# app/models/account.rb
class Account < ActiveRecord::Base
  has_many :account_users
  has_many :users, through: :account_users
end

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :account_users
  has_many :accounts, through: :account_users
end

And the join model of course:

# app/models/account_user.rb
class AccountUser < ActiveRecord::Base
  belongs_to :account
  belongs_to :user
end

Up until now everything's ok. I can access associated records from each endpoint of the association, and Rails handles the SQL join, like we're all used to. Hell, I can even create associated records with a single command, and ActiveRecord creates the intermediate AccountUser record for me. It's heaven.

> account = Account.create(name: "Main")
=> #<Account id: 1, name: "Main", ...>
> account.users.count
=> 0
> user = account.users.create(name: "johndoe")
=> #<User id: 1, name: "johndoe", ...>
> user.accounts.count
=> 1

The problem occurs when trying to build the record first in memory, and saving it later:

> user = account.users.build(name: "uglyjoe")
=> #<User id: nil, name: "uglyjoe", ...>
> user.save
=> true
> user.accounts.count
=> 0

The user record was saved, but it was not associated to the account.

The solution

After a while digging, I found this issue on Rails' github repository, and specifically, this comment. It turns out that in these cases, you need to specify the :inverse_of option to the belongs_to association in the intermediate join model, in our case, the AccountUser class.

# app/models/account_user.rb
class AccountUser < ActiveRecord::Base
  belongs_to :account
  belongs_to :user, inverse_of: :account_users
end

Note that you only have to specify the inverse on the association that links with the model being created. If you ever want to create accounts from the user model, then you also have to specify the inverse to the belongs_to :account association. However, my recommendation would be to always specify the inverse to both associations in a join model like this one.

But wait, there's more

It turns out that the :inverse_of option is there for even more reasons. After discovering the solution above, I decided to dig a bit deeper about it, and I found out that it is always desirable to declare the inverse on all belongs_to associations. It optimizes the way Rails instantiates ActiveRecord objects. I won't go through the details, but you can take a look here, here and here if you're interested.

By the way, I still don't know why Rails doesn't make all these :inverse_of goodness work out of the box when it could infer the inverse association in cases like the one above. I'm sure there must be a reason though.