• Rails 4: How to set parent model on both left and right side in a has_many :trough relationship, models being created with nested attributes

Let’s say you have a many-to-many setup, with same parent for both left and right side. You might want to create an account, with a user and a project in one go.

The setup

# app/models/account.rb
class Account < ActiveRecord::Base
  has_many :users
  has_many :projects

  accepts_nested_attributes_for :users
# app/models/user.rb
class User < ActiveRecord::Base
  belongs_to :account

  has_many :user_projects, :class_name => "User::Project", dependent: :destroy, inverse_of: :user
  has_many :projects, through: :user_projects

  validates_presence_of :account, on: :create

  # We want to accept nested attributes for projects
  accepts_nested_attributes_for :project
# app/models/user/project.rb
class User::Project < ActiveRecord::Base
  belongs_to :user, class_name: "User", inverse_of: :user_projects
  belongs_to :project, class_name: "Project", inverse_of: :project_users

  validates_presence_of :user, :project

  # No need for duplicates
  validates_uniqueness_of :project, scope: :user_id
# app/models/project.rb
class Project < ActiveRecord::Base
  belongs_to :account

  has_many :project_users, :class_name => "User::Project", dependent: :destroy, inverse_of: :project
  has_many :users, through: :project_users

  validates_presence_of :account, on: :create

The issue

When you try to create an account with a default user and project you will encounter an issue. The parent account is not set for the project, but it is set for the user. In the above setup it will actually throw a validation error.

It is because the inverse_of value isn’t kept throughout the has_many: through relationship.

The fix was to add the following before_validation rule to the User model (assuming that you create the user first, and then the project as nested attribute to the user).

  before_validation :set_account_for_new_projects
  def set_account_for_new_projects
    self.user_projects.each do |user_project|
      # If id is not set, it means this is a new project
      user_project.project.account = self.account if !user_project.project.id

The Author

Dan Schultzer is an active experienced entrepreneur, starting the Being Proactive groups, Dream Conception organization, among other things. You can find him at twitter

Like this post? More from Dan Schultzer

Comments? We would love to hear from you, write us at @dreamconception.