Wednesday, April 12

has_many :headaches

Continuing my rant from earlier, I decided to delve into the source and see if I could figure out exactly what's going on, and maybe by some miracle add the functionality I need and expected.

First stop was to find out where the "<<" operator was being overloaded. Yes, I know that in Ruby it's not an operator, it's a message, and it's not overloaded. But I'm an old fart, and those are the terms I was weaned on, so deal with it. But I digress.

I found the magic in this file:

rails/activerecord/lib/active_record/associations/association_collection.rb

And the code looks like this:

module ActiveRecord
module Associations
class AssociationCollection < AssociationProxy

...

def <<(*records)
result = true
load_target

@owner.transaction do
flatten_deeper(records).each do |record|
raise_on_type_mismatch(record)
callback(:before_add, record)
result &&= insert_record(record) unless @owner.new_record?
@target << record
callback(:after_add, record)
end
end

result && self
end

Pretty straight forward. It looks like it's defering to a method named "insert_record" to handle the databaseness, so that was my next stop, and I found it in this file:

rails/activerecord/lib/active_record/associations/has_many_association.rb

And here's the code:

module ActiveRecord
module Associations
class HasManyAssociation < AssociationCollection

...

def insert_record(record)
set_belongs_to_association_for(record)
record.save

end

Now some of the pieces are starting to fall into place. What it's doing here is only going to (and only does) work for a one-to-many relationship. What is does is takes the record passed in to the collection, sets its foreign key value, then saves the record.

What I need it to do is recognize that in some cases it's dealing with a many-to-many relationship and rather than saving the record that is passed into the collection it needs to create the record for the cross-reference.

Now the code above should have all of the information it needs to do just that. After all, it built the collection by querying the right tables, so it knows all of the tables names and their important columns, so it should be able to write that relationship back into the database. I need to dig a little more to figure out how I can leverage that.

I'm a bit surprised it hasn't already been done, which leads me to fear that it's rather ugly or nigh-impossible to accomplish, but I'm holding out hope. I'm hoping that somebody in the know stumbles across these rants and offers a helping hand in either making it work or explaining why it wasn't built to do so.

3 comments:

Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...

This is an interesting issue. I keep finding that the has_many :through stuff isn't complete in all sorts of ways. But I think what you're looking for might be a lot harder than you think. Why? Because the join model row you want Rails to create automatically can have arbitrary data besides the reference to the object you want to add to the collection.

I think a good way to handle this would be to use an association extension method to create the entry in the join model table for the new object and set up the other attributes as necessary. I've almost run into needing that myself already.

Teflon Ted said...

"Because the join model row you want Rails to create automatically can have arbitrary data besides the reference to the object you want to add to the collection."

Yes, another reader made the same comment on my prior post, and I agree, but in the spirit of Rails convention over configuration I think a "most cases" solution would be adequate.

"I think a good way to handle this would be to use an association extension method to create the entry in the join model table for the new object"

Yes. I'm investigating that now, but I've not quite put all the pieces together yet. I'm having a hard time reverse engineering the magic going on in ActiveRecord::Associations::AssociationCollection.

Thanks for the note.