Lighthouse has a new layout. Prefer the old one? Return to the old layout, and switch back any time from the link at the top of each page.

This project is archived and is in readonly mode.

habtm: collection.build doesn't build join associations (unlike collection.create)

#220

I have a habtm relationship between two models. When I try to #build one object through the collection helper of a record that persists in the database, it doesn't add any ids to that record. On the flip side, #create does store the ids.

Example:

class Project < ActiveRecord::Base
  has_and_belongs_to_many :users
end

class User < ActiveRecord::Base
  has_and_belongs_to_many :projects
end

# A db-persistent user record

user = User.find(10)

# collection.build

project = user.projects.build
project.users # [] (empty)
project.user_ids # [] (empty)

# collection.create

project = user.projects.create
project.users # [#<Account id: 10 ...>]
project.user_ids # [10]

Reported by bentlegen · May 18th, 2008 @ 08:54 PM

State: stale
Milestone: none
Assigned to: nobody
Importance: none

Activity

  1. Christopher Swasey (endash)
    Christopher Swasey (endash)

    Could you clarify what version of rails you are using? Edge rails includes tests which test this functionality, and they all pass.

    Also, #build doesn't save the created records, and so they won't have IDs.

    May 19th, 2008 @ 03:38 AM

  2. bentlegen
    bentlegen

    activerecord (2.0.2)

    The problem is that, even if I call #save after #build, the associations aren't saved in the join table. The AR documentation suggests they should.

    I'll give Edge Rails a try.

    May 19th, 2008 @ 03:51 AM

  3. Christopher Swasey (endash)
    Christopher Swasey (endash)

    I misread the ticket, and I was wrong about the tests covering this case.

    May 19th, 2008 @ 03:51 AM

  4. Christopher Swasey (endash)
    Christopher Swasey (endash)

    From what I can tell, this isn't a bug:

    When you #build off an association, the join table is updated when you call #save on the object from which you called #build.

    The association collects all unsaved members, calls #insert_record for each record, and upon saving the record (and getting an ID to use in the join table) it creates the join table record with the newly gotten ID, and the pre-existing ID of the record upon through you called #build.

    Basically, what you're trying to do is to use the association in reverse. On a related note, ActiveRecord will not allow you to call #build through an association belonging to an unsaved object. Which is kind of what you're trying to do, in effect.

    May 19th, 2008 @ 04:10 AM

  5. bentlegen
    bentlegen

    Thanks for the explanation Chris - I understand now.

    Still, it seems odd to me that these two code examples behave differently:

    project = user.project.build
    project.save
    
    # vs
    
    project = Project.new(:user_id => user)
    project.save
    

    I prefer the syntax of the first example, personally. Maybe I'll look into creating a patch.

    May 19th, 2008 @ 04:20 AM

  6. bentlegen
    bentlegen

    Sorry - my bad, that 2nd example wouldn't work with habtm.

    May 19th, 2008 @ 10:21 AM

  7. Christopher Swasey (endash)
    Christopher Swasey (endash)

    A quick look through the source seems to suggest that simply adding the existing record to the association collection of the new record would create the desired symmetry.

    Then, calling #save on the new object would save it, and because it was a new_record before being saved, it's entire association collection would then be passed one at a time to insert_record, which would then save the existing record and then create the join table record.

    I think that would do it, but that's just from 15 minutes of looking at the applicable source files.

    May 19th, 2008 @ 04:47 AM

  8. Christopher Swasey (endash)
    Christopher Swasey (endash)

    Ah... the two records want to validate each other. That results in a nice runaway loop.

    Code could be added to the validation to prevent looping, but I'm wondering if its worth the trouble. Something tells me this sort of change would turn into the programming equivalent of quickand: the more you struggle, the deeper you get, until there's no escape.

    May 19th, 2008 @ 05:27 AM

  9. Christopher Swasey (endash)
    Christopher Swasey (endash)

    I am just all kinds of wrong tonight. You can, of course, call #build on a new record just fine. (it's #create that you can't call on a new record)

    May 19th, 2008 @ 06:09 AM

  10. josh
    josh
    • State changed from new to stale
    • Tag set to activerecord, bug, has_and_belongs_to_many

    August 20th, 2008 @ 01:52 AM

  11. jack dempsey (jackdempsey)
    jack dempsey (jackdempsey)

    Is there a reason this was marked as stale? I'm seeing very something similar on rails 2.3.2. The bottom line is that record.habtm_association.create(...) does NOT work like record.habtm_association.build(...).save as it implies in the docs.

    Tested this a few ways locally and can confirm it consistently. If this is expected behavior, then we really should update the docs. If it should work this way, then it sounds like a patch is in order. Either way, this cost me a good part of a day, and I'd appreciate moving this back to new or open.

    July 14th, 2009 @ 08:36 PM

  12. Jonas Schneider
    Jonas Schneider

    This is still an issue on Rails 2.3.5. Caused me a lot of headaches in the past days.
    Full reproduction steps and a failing test case for added pleasure are in this gist:

    http://gist.github.com/290695

    January 30th, 2010 @ 07:59 PM

  13. Rolf Bjaanes
    Rolf Bjaanes

    Had a quick look at the gist, and this is not a bug.

    You are saving the instance of the associated object,
    but not the object carrying the association to be saved.

    Following assumes habtm relations for projects and users.

    
      @user = User.first
      @project = @user.projects.build(:name => "Make your bed")
    
      # if you do @project.save here, as in the gist,
      # association will not be saved, but if you do :
    
      @user.save
    
      # it will save the @user with its associated projects
    
    

    April 14th, 2010 @ 06:34 PM

  14. bingbing