15

i have three models, all for a has_many :through relationship. They look like this:

class Company < ActiveRecord::Base

  has_many :company_users, dependent: :destroy
  has_many :users, through: :company_users

  accepts_nested_attributes_for :company_users, :users

end

class CompanyUser < ActiveRecord::Base
  self.table_name = :companies_users #this is because this was originally a habtm relationship
  belongs_to :company
  belongs_to :user
end

class User < ActiveRecord::Base
  # this is a devise model, if that matters

  has_many :company_users, dependent: :destroy
  has_many :companies, through: :company_users

  accepts_nested_attributes_for :company_users, :companies

end

this loads fine, and the joins are built fine for queries. However, whenever i do something like

@company = Company.last
@user = @company.users.build(params[:user])

@user.save    #=> true
@company.save #=> true

both the User record and the CompanyUser records get created, but the company_id field in the CompanyUser record is set to NULL

INSERT INTO `companies_users` (`company_id`, `created_at`,`updated_at`, `user_id`) 
VALUES (NULL, '2012-02-19 02:09:04', '2012-02-19 02:09:04', 18)

it does the same thing when you @company.users << @user

I'm sure that I'm doing something stupid here, I just don't know what.

rm-rf
  • 1,313
  • 1
  • 15
  • 24

3 Answers3

20

You can't use a has_many :through like that, you have to do it like this:

@company = Company.last
@user    = User.create( params[:user] ) 
@company.company_users.create( :user_id => @user.id )

Then you will have the association defined correctly.

update

In the case of the comment below, as you already have accepts_nested_attributes_for, your parameters would have to come like this:

{ :company => 
    { :company_users_attributes => 
        [ 
          { :company_id => 1, :user_id => 1 } ,
          { :company_id => 1, :user_id => 2 },
          { :company_id => 1, :user_id => 3 } 
        ]
    } 
}

And you would have users being added to companies automatically for you.

Maurício Linhares
  • 39,901
  • 14
  • 121
  • 158
  • so if you wanted to allow someone to edit a company and allow them to add users with checkboxes i would need to pull the user_ids array out of the params hash and create a CompanyUser record for each one? – rm-rf Feb 19 '12 at 04:29
  • Thanks! that was what I was missing. I didn't even think about making the form like that. called it on me being stupid. – rm-rf Feb 19 '12 at 05:01
  • 2
    `@company.users.create(params[:user])` also works (at least in Rails 4). For some reason the `@user = @company.users.build(params[:user]); @user.save` doesn't set the association for me. – toobulkeh May 08 '15 at 02:14
  • 1
    This answer is technically incorrect when it states that 'You can't use a has_many :through like that'. Not only you can, but it's officially supported and mentioned in the docs, as long as you set the :inverse_of in the join model. Read here (https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html) in the section 'Setting Inverses', where it clearly states an example just like the one here. – sandre89 May 28 '19 at 13:26
8

If you have a has_many :through association and you want to save an association using build you can accomplish this using the :inverse_of option on the belongs_to association in the Join Model

Here's a modified example from the rails docs where tags has a has_many :through association with posts and the developer is attempting to save tags through the join model (PostTag) using the build method:

@post = Post.first
@tag = @post.tags.build name: "ruby"
@tag.save

The common expectation is that the last line should save the "through" record in the join table (post_tags). However, this will not work by default. This will only work if the :inverse_of is set:

class PostTag < ActiveRecord::Base
  belongs_to :post
  belongs_to :tag, inverse_of: :post_tags # add inverse_of option
end

class Post < ActiveRecord::Base
  has_many :post_tags
  has_many :tags, through: :post_tags 
end

class Tag < ActiveRecord::Base
  has_many :post_tags
  has_many :posts, through: :post_tags  
end

So for the question above, setting the :inverse_of option on the belongs_to :user association in the Join Model (CompanyUser) like this:

class CompanyUser < ActiveRecord::Base
  belongs_to :company
  belongs_to :user, inverse_of: :company_users
end

will result in the following code correctly creating a record in the join table (company_users)

company = Company.first
company.users.build(name: "James")
company.save

Source: here & here

Kareem Grant
  • 96
  • 1
  • 3
6

I suspect your params[:user] parameter, otherwise your code seems clean. We can use build method with 1..n and n..n associations too, see here.

I suggest you to first make sure that your model associations works fine, for that open the console and try the following,

> company = Company.last
=> #<Tcompany id: 1....>
> company.users
=> []
> company.users.build(:name => "Jake")
=> > #<User id: nil, name: "Jake">
> company.save
=> true

Now if the records are being saved fine, debug the parameters you pass to build method.

Happy debugging :)

nkm
  • 5,844
  • 2
  • 24
  • 38