Topic: Accessing attributes on join table?

Is there any way to access attributes on a join table used in has_many :through relationship?

For example:
An article has_many :readers through :readings.
A reader has_many :articles through :readings.

There are attributes on the readings join table (which is itself a model) such as rating and review:
Table readings:
id int
article_id int
reader_id int
rating int
review mediumtext

If you are accessing a particular reader.article, is there any way to access the attributes of the reading record (such as rating and review)? E.g.:

for reader in Readers.find(:all)
  reader.article.rating  (this won't work, but it's the desired result)


That way, you can have a view spit out something like:
John Doe has read three articles:
Bla bla (4 stars)
Yada yada yada (2 stars)
Ok Ok Ok (1 star)


The only solution that has been suggested to me is to traverse the readings directly, which is sort of missing the entire point of the :through relationship.  In that case, I basically have to rewrite the logic that rails is supposed to be doing for me with the :through concept, by iterating over all of the readings and screening out only that subset with the reader_id I care about. 

I see discussion in some pages found through my googling that the has_and_belongs_to_many method has a deprecated feature that allows you to mess with attributes on the join table (push_with_attributes).  I don't think what I am trying to is particularly novel.  Even just getting the ID of the record in the join table that connected this article to this reader would be enough or me to subsequently get at the attributes of the join table that I care about.

Thoughts?
-angus

Re: Accessing attributes on join table?

I don't understand the problem. You can't call reader.article because each reader has_many articles. You would need to iterate through the given articles in the first place, so it's really no different to iterate through the readings and then fetch the article through that.

Railscasts - Free Ruby on Rails Screencasts

Re: Accessing attributes on join table?

I'd approach it something like this (not tested)

reader.readings.each do |r|
<%= r.article.title+" "+r.rating+"<br/> %>
end

So use an array of the readings belonging to a particular reader as the starting point.

Is this what yo uwanted? (edit: this is what ryan suggested)

Last edited by cbit (2006-11-25 17:32:59)

Re: Accessing attributes on join table?

Well, the reason why I would like to avoid iterating through all the items in the join table is as follows...

I imagine a has_many :through along the following lines, with the key part being a set of conditions.

#Article:
  has_many :readings
  has_many :readers, :through => :readings

#Reader:
  has_many :readings
  has_many :articles, :through => :readings
  has_many :favorite_articles, :through => :readings,
           :conditions => "readings.favorite = 1"

#Reading:
  belongs_to :article
  belongs_to :reader

Importantly, assume the join table "readings" has the following five columns: id, article_id, reader_id, favorite, remarks.  Assume the articles table has two columns: id and title.


Now, imagine in your view, you are printing a page with a reader profile, and there is a list of that reader's favorite articles.  You might have code along the following lines:

<% for favorite_article in @reader.favorite_articles -%>
  <li><%= favorite_article.title -%></li>
<% end %>

That would produce a nice list of the reader's favorite articles.  The underlying query to the database for a list of matching records in the "readings" table to match a particular value of person_id and favorite would have been performed exactly once. 

However, if you want to show the reader's remarks, you need to do the query differently. Everyone tells me the only way to get at any attribute of the join table is to traverse the join table directly -- i.e. I cannot get at attributes of the join table merely by referencing some item was accessed through that very join relationship.  Consider the code we need to write in order to show the view of a reader's favorite articles, including the remarks that reader recorded for a given reading:

<% for reading in Readings.find(:all, :conditions => "person_id ='#{params[:id]}', favorite='1'") %>
  <li><%= Articles.find(reading.article_id).title -%> (<%= reading.remarks -%>)</li>
<% end %>

The moral of the story here seems to be that if you have any sort of interesting attribute you want to access in the join record, it is pointless to define some sort of conditions-based thing like ":favorite_articles" in our Reader model, because you are simply going to rewrite the SQL yourself later on, i.e. you will never use the ":favorite_articles" construct if you end up needing to access an attribute of the join table.  That is, unless you want to repeat yourself. 

I guess that is what I was getting at.  Maybe I am completely wrong, in which case apologies for being a newbie.  But what I had been hoping was that since by definition an individual favorite_article from Reader.favorite_articles (through readings) came into being through a specific join, I might get some shortcut way of accessing that join record and hence writing much cleaner code along the following lines:

<% for favorite_article in @reader.favorite_articles -%>
  <li><%= favorite_article.title -%> (<%= [something here].remarks -%> )
<% end %>

Where "something here" would be a shorthand way of saying, look, the very existence of favorite_article is as a result of some record in the intermediate join table ("readings"), so give me a reference to that record...  And remember, Readings.find(:all, :conditions=>...) won't work in place of "[something here]" because the same person can read the same article multiple times, with different remarks each time.

-angus

Re: Accessing attributes on join table?

Liberty248 wrote:

And remember, Readings.find(:all, :conditions=>...) won't work in place of "[something here]" because the same person can read the same article multiple times, with different remarks each time.

At first I was going to suggest creating a favorite_readings (instead of favorite_articles), but after reading your last statement I see why this would not work.

From my understanding, if the reader has many remarks for the same article, you only want to display the title of the article once and list the remarks below that. This means you will need to iterate through the remarks/readings for each article. Something like this should work:

<% for article in @reader.favorite_articles %>
  <%= article.title %>
  <% for reading in article.readings.find_all_by_reader_id(@reader.id) %>
    <%= reading.remarks %>
  <% end %>
<% end %>

You may want to remove the find_all_by... method into the Article model to make it shorter:

def readings_for_reader(reader)
  readings.find_all_by_reader_id(reader.id)
end

This will probably result in another query to the database so you may want to optimize that by looping through the already found readings and returning any for the given reader.

I understand if this entire solution is not satisfactory, but I don't see how there could be a better way. The article itself doesn't have any knowledge as to which reader it was fetched through, and that's how it should be IMO. I don't think a model's state should change depending upon how it is fetched.

Railscasts - Free Ruby on Rails Screencasts