Quick question on updating attributes on the join model in a has_many :through relationship

The question is this: how are you supposed to do it?

I’ve seen a lot of blog posts that say that HM:T has beaten HABTM and that we can now all easily have data on the join model and go on to show how to enter that data into the join model but don’t show a recommended way of modifying it later.

I’ve checked against a pile of books, too, as well as the AR api and Google’s exhaustive collection of blogs. Neither The Rails Way nor Agile Web Development show an example that I can find.

Obviously this is something really simple that I have missed so if someone could please post a link to the place where it is actually demonstrated that would be wonderful.

Taking the example from http://blog.hasmanythrough.com/2006/8/19/magic-join-model-creation if you’re unsure what I mean:

That automagically creates the join record with the role attribute set to “author”. Pretty nifty, eh? But we can do better.

class Contribution < ActiveRecord::Base
  belongs_to :book
  belongs_to :contributor
  belongs_to :author, :class_name => "Contributor"
  belongs_to :editor, :class_name => "Contributor"

end
class Book < ActiveRecord::Base
  has_many :contributions, :dependent => :destroy
  has_many :contributors, :through => :contributions, :uniq => true
  has_many :authors, :through => :contributions, :source => :author,

                          :conditions => "contributions.role = 'author'" do
      def <<(author)
        Contribution.with_scope(:create => {:role => "author"}) { self.concat author }

      end
    end
  has_many :editors, :through => :contributions, :source => :editor,
                          :conditions => "contributions.role = 'editor'" do
      def <<(editor)

        Contribution.with_scope(:create => {:role => "editor"}) { self.concat editor }
      end
    end
end

Then give this a shot…

dave = Contributor.create(:name => "Dave")
chad = Contributor.create(:name => "Chad")
awdr = Book.create(:title => "Agile Web Development with Rails")
awdr.authors << dave

awdr.editors << chad

That’s brilliant (although I suspect it will break now because at some point with_scope got removed from public so I’m not sure what people suggest nowadays).

What if you added another field to contributions? Let’s say, for lack of a better example, time_spent. This is how you are supposed to find it, as far as I can tell:

dave = Contributor.find_by_name “Dave”

book = Book.find_by_title “Agile Web Development with Rails”

contribution = Contribution.find_by_book_id_and_author_id book.id, dave.id

And then you can now go

contribution.time_spent = 9.months

But surely there is a way to get that contribution object out of the collection proxy? Something like:

contribution = dave.books.proxy_object(dave.books.first)
or

contribution = dave.books.proxy_objects.first

(Yes, I realise this would be essentially the same thing, but the method I showed first strikes me as being very non-rails).

So, is there some wonderful resource where this has been demonstrated that I’ve just missed?

Cheers,
Morgan.

Is this question too confusing or too obvious to warrant a response from somebody?

Morgan.

Well, with HM:T, you’ve got an actual model to work with–so you can do all the regular things you do w/models–.new them, .update them, etc. In a view, you’d do a form_for & pass in a reference to an instance o your join model. The things you’re joining can be represented via say, collection_select’s modifying the respective <>_id fields in your join model.

If the goal is allowing create/edit on views devoted to one or both of the joined models, you can treat those exactly as if they were just a regular has_many–so, field_for, w/an appropriately engineered name. See the railscasts on “complex forms” for the gory details.

HTH,

-Roy

Thanks for that.

The basic answer is that you just have to manually find the join model and then modify that as necessary. Which is what I was doing but which I thought was silly because hey, this is Rails, there has to be an easier way to do it. :slight_smile:

Cheers.

Well, not necessarily–in those railscasts I mentioned, they show how to use pseudo-attributes on one of the ‘parent’ models to create/update/delete associated instances of a join model. For example, here’s a bit of my Project model class (which represents research projects) that manages the associations between projects and research areas:

class Project < ActiveRecord::Base
  has_many :projects_research_areas, :dependent => :delete_all
  has_many :research_areas, :through => :projects_research_areas
  def new_research_area_attributes=(raa)
    raa.each do |ra|
      projects_research_areas.build(ra)
    end
  end
  def existing_research_area_attributes=(raa)
    projects_research_areas.each do |ra|
      attributes = raa[ra.id.to_s]
      if attributes
        ra.attributes = attributes
        ra.save
      else
        projects_research_areas.delete(ra)
      end
    end
  end
end # class Project

Those *research_area_attributes=() methods get called (as if by magic :wink: when the project controller says e.g., Project.new(params[:project]) and they create/update/delete associated instances of the join model as necessary. You direct a given instance of a join model to either the new or existing_ version by setting the first argument to fields_for() to the right string. Check out railscasts 73-75 (IIRC) for the full story. See especially the updated code posted on the episode 75 page.

HTH,

-Roy