#220 √ stale
bentlegen

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

Reported by bentlegen | May 18th, 2008 @ 08:51 PM

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]

Comments and changes to this ticket

  • Christopher Swasey (endash)

    Christopher Swasey (endash) May 19th, 2008 @ 03:38 AM

    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.

  • bentlegen

    bentlegen May 19th, 2008 @ 03:51 AM

    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.

  • Christopher Swasey (endash)

    Christopher Swasey (endash) May 19th, 2008 @ 03:51 AM

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

  • Christopher Swasey (endash)

    Christopher Swasey (endash) May 19th, 2008 @ 04:10 AM

    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.

  • bentlegen

    bentlegen May 19th, 2008 @ 04:20 AM

    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.

  • bentlegen

    bentlegen May 19th, 2008 @ 10:21 AM

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

  • Christopher Swasey (endash)

    Christopher Swasey (endash) May 19th, 2008 @ 04:47 AM

    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.

  • Christopher Swasey (endash)

    Christopher Swasey (endash) May 19th, 2008 @ 05:27 AM

    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.

  • Christopher Swasey (endash)

    Christopher Swasey (endash) May 19th, 2008 @ 06:09 AM

    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)

  • Joshua Peek

    Joshua Peek August 20th, 2008 @ 01:52 AM

    • → Tag changed from “” to “activerecord bug has_and_belongs_to_many”
    • → State changed from “new” to “stale”

Please Login or create a free account to add a new comment.

You can update this ticket by sending an email to from your email client. (help)

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile »

Source available from github

The Git repository resides at http://github.com/rails

Check out the current development trunk (Edge Rails) with:

git clone git://github.com/rails/rails.git

Creating or reviewing a patch

See the contributor guide.

Creating a feature request

Please don't. If you want a new feature in Rails, you'll have to pull up your sleeves and get busy yourself. Or convince someone else to do it. See the contributor guide on how to get going. But posting them here is just going to lead to ticket root.

Creating a bug report

When creating a bug report, be sure to include as much relevant information as possible. Post the code sample that causes the problem. Preferably, alter the unit tests and show through either changed or added tests how the expected behavior is not occuring.

Security vulnerabilities should be reported via an email to security@rubyonrails.org, do not use trac for reporting security vulnerabilities. All content in trac is publicly available as soon as it is posted.

Then don't get your hopes up. Unless you have a "Code Red, Mission Critical, The World is Coming to an End" kinda bug, you're creating this ticket in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the ticket automatically will see any activity or that others will jump to fix it. Creating a ticket like this is mostly to help yourself start on the path of fixing the problem and for others to sign on to with a "I'm having this problem too".

Shared Ticket Bins