has_many through new_record? exception

Hello all,

I was hoping someone could explain to me why both sides of a has_many :through association need to not be new_records? Wouldn't the save be able to succeed if the object with the has_many :through association was the only new_record?

It seems to me that because something like

p = posts.new p.taggings << Tagging.new(:tag_id => 1) p.save

works, that something like

p = posts.new p.tags << Tags.create p.save

should be able to work since rails should be able to do essentially what happens in the first example under the covers.

Maybe it's difficult to manage what p.tags returns before you've saved it, or something like that? Is it the sort of thing that could be done, it's just high effort, or is there some impossible case I'm missing?

After I started thinking about it I became very curious, so I googled around, searched trac, but didn't find anything. I'd be very appreciative for any enlightenment on the subject.

Thanks, Jason

The new_record association stuff in ActiveRecord is pretty hairy, and doesn't always work as expected. It's a poor man's Unit-of-Work[1] but only for special circumstances. The new_record hack only works when the containing model (the one with the has_many) and the content models (the ones with belongs_to) are both new. Instead of setting the fk (e.g. the container_id field) in the content object, the association waits until the container is saved and gets a primary key. Then it saves all the content objects with that new container pk as the content fk.

The intractable problem with has_many :through is that you would have to order a bunch of saves to get everything to happen correctly. For your example, you'd have to save all the tag and post records first, then you could save the tagging records. But there is no UnitOfWork to help you out, that is, no global knowledge of all the new tags and posts that need saving. You could try starting with one post and saving it, but then you'd have to go through its taggings to save their tags too, since you need both tag and post saved before you can set the fks in the tagging and save it. Since each tag may have many taggings, you can get into an ordering mess that is impossible (or maybe just very difficult) to linearize, at least given the constraints of how ActiveRecord works.

[1] P of EAA: Unit of Work

Thank you very much for responding. Unfortunately, I'm still confused.

I'm not sure exactly what you're talking about when you say "he new_record hack". Are you referring to the code that allows me to save a 'new' tag and many 'new' taggings all in one go?

I'm still not sure what the difficulty is though in the specific case where all the Tags already exist in the DB. My usage of Tag.create (Tags was a typo) in my example was intended to signify that the Tag would already exist in the DB so the Taggings that rails would create when saving the Post would be able to use the Tag's id.

If the Tag already exists then shouldn't the tagging that rails makes up to associate with the Post be able to use it's id, thereby making it essentially the same as if I this?

post.taggings << Tagging.new(:tag_id => n)

I guess a related question, or maybe it's the same question in the end, is why the has_many_through_association (http://dev.rubyonrails.org/browser/trunk/activerecord/lib/active_record/associations/has_many_through_association.rb) '<<' method uses 'create!' instead of 'new'?

Thanks, Jason

First of all, you cannot call << on when parent is a new record.

p = posts.new p.taggings << Tagging.new(:tag_id => 1) p.save

That'd raise an exception.

I know it raises an exception the way it's coded right now, my question is why? Why was the design choice made to work this way, instead of allowing "post.tags << Tag.create" to work, since the tag would have an id? I think it would be mighty handy if it worked, so I'd certainly consider trying to create a patch, I just wanted to know if there's some solid rationale behind the current method that I'm missing.

Thanks, Jason

What's the solid rationale behind trying to create tags for unsaved post ? Why can't you just do post.save first and then do post.tags << Tag.create ? The current behavior is to make sure you don't end up having dangling children.

And believe me, many people found some defect in their apps when the current behavior was introduced.

Well, mapping my particular situation onto Tags and Posts, I don't care at all if I have a Tag that doesn't have any posts, but I insist that Posts be tagged. So right now I'm forced to save the Post without its Tag validation, then add the Tag (or Tagging as it were) and save again. It's a real hassle if happens that makes the Post invalid on the second save, then I have to clean up from the first save. It seems like that's more work, and less readable than if "post.tags << Tag.create" worked.

I'd like to be able to display a form for a Post, and show its Tags, without having to touch the DB, and without having to jump through association hoops.

Mostly I'm just curious if anyone can tell me why it works the way it does right now since at the very least it means writing a few more lines of code.

Thanks, Jason

Can you just wrap everything in a transaction? The you don't have to do any manual cleanup.

But I agree, it feels funny to have to save the Post in a seemingly invalid state up front.

Jeff softiesonrails.com

Yeah, as it is right now I'm doing it with transactions, and some extra validation, but it'd be great if I could get rid of the extra code.