Bradley Priest

Accessing attributes from has_many :through join models 18 Mar 2012

I’ve been trying to help out a bit in the Rails section on Stack Overflow lately and have noticed a question that has come up several times lately.

Accessing an attribute from the join model on a has_many :through relationship. For the basics, check out the Rails guide on the subject.

Here’s a pretty standard application of this in my awesome Library SAAS app:

class Reader < ActiveRecord::Base
  has_many :book_loans
  has_many :books, :through => :book_loans
end

class Book < ActiveRecord::Base
  has_many :book_loans
  has_many :readers, :through => :book_loans
end

class BookLoan < ActiveRecord::Base
  belongs_to :book
  belongs_to :reader
end

class BookController < ApplicationController
  def show
    @book = Book.includes(:readers).find(params[:id])
  end
end

And on the books#show page we want to show all the people who have checked out a particular book.

  <%= @book.name %>
  Readers:
  <ul>
    <% @book.readers.each do |reader| %>
      <li><%= reader.name %></li>
    <% end %>
  </ul>

Now what if I want to add the librarian who checked out the book to them to the page. This is stored as the librarian column on the BookLoan model, which we don’t actually have access to in the view at the moment.

This is my current solution to the problem, it involves a bit of SQL which might be a little scary for beginners, please leave a comment if you’ve got a better idea, I may even look into seeing if it’s achievable with ARel sometime soon.

  class Book < ActiveRecord::Base
    has_many :readers, :through => :book_loans,
             :select => "readers.*, book_loans.book_loan_librarian AS book_loan_librarian"
  end
  <% @book.readers.each do |reader| %>
    <li><%= reader.name %> by <%= reader.book_loan_librarian %></li>
  <% end %>

If anyone has any questions/suggestions feel free to leave a comment or hit me up on Twitter.

N.B. I was originally going to use book_loan.created_at but ActiveRecord doesn’t automatically typecast the extra columns returned which would be a bit out of the scope of this post.

EDIT: In Rails 4+ it would look like this

   has_many :readers, -> { select("readers.*, book_loans.book_loan_librarian AS book_loan_librarian") },
            :through => :book_loans

TIL The very, very long way 19 Sep 2011

Whilst upgrading an application to Rails 3.1 recently I ran into one annoying heisenberg.

Long story short, do not use “stream” as a controller action name in 3.1 as it is used by the new HTTP streaming.

ActiveRecord Ranges 18 Jun 2011

Just a quick one today, I’m going to mention a quick trick you may have heard about, but is definitely worth knowing.

When using ActiveRecord as well as passing a String/Integer or Array into a query you can also use a Range.

I find this particularly helpful when searching by date.

For example instead of:

  Widget.where('created_at > ? AND created_at < ?', 2.hours.ago, Time.now)
    #=> SELECT "widgets".* FROM "widgets" WHERE (created_at > '2011-06-18 03:38:58.493361' AND created_at < '2011-06-18 05:38:58.493442')

You can use a range, e.g.

  Widget.where(:created_at => 2.hours.ago..Time.now)
    #=> SELECT "widgets".* FROM "widgets" WHERE ("widgets"."created_at" BETWEEN '2011-06-18 03:36:53.551349' AND '2011-06-18 05:36:53.551489')

Notice how using an inclusive range produces a SQL BETWEEN query.

Using an exclusive range gives a different query.

  Widget.where(:created_at => 2.hours.ago...Time.now)
    #=> SELECT "widgets".* FROM "widgets" WHERE ("widgets"."created_at" >= '2011-06-11 05:25:12.738961' AND "widgets"."created_at" < '2011-06-18 05:25:12.739321')

Use carefully.

For an interesting use of this check out one of the new methods added to Rails 3.1 check out Date.today.all_day added here.