views:

280

answers:

3

Rails and ActiveRecord do a perfectly nice job of dealing with what I would consider exceedingly simple queries that deal with a single model. I now have a situation where I need to do something slightly more complex - but still trivial - and I don't know how to do it.

I have a User that has_many :images. Each Image has_many :thumbnails.

On my /images index page, I actually want to display the small thumbnail for each image that belongs to the logged in user. An easy query, but because doing so involves conditions on multiple tables, I'm not sure how to approach it in true Rails fashion. I'd prefer to avoid writing SQL and dealing with the ensuing headaches.

I see that rails offers the :joins option, but that seems like a pretty inelegant afterthought. Is this the best way to meet this kind of requirement or is there a better way that I just haven't found?

Thanks.

UPDATE: I've been told about named scopes and have to admit to having some very inappropriate feelings for them right now. These allow fairly complex conditions to be applied very elegantly with lots of syntactic sugar. For example, I can create a named scope on my Image model for images owned by a given user and make it dynamic:

class Image < ActiveRecord::Base
  named_scope :owned_by, 
              lambda { 
                |user_id| { 
                  :conditions => { :user_id => user_id } 
                } 
              }
end

I can also apply a named scope on my Thumbnail model to specify the small thumbnail:

class Thumbnail < ActiveRecord::Base
  named_scope :small, :conditions => { :name => 'Small' }
end

Now I can chain those together to do some pretty powerful stuff that reads beautifully. In my controller, I can return all of the images for a given user:

@images = Image.owned_by( current_user )

In my view, though, I want to display the small thumbnail, so we chain mightily:

<% for image in @images %>
  <tr>
    <td><%= h( image.name ) %></td>
    <td><%= link_to( image_tag( image.thumbnails.small.first.binary.uri, :alt => h( image.name ), :title => h( image.description ) ), :action => :show, :id => image.id ) %></td>
  </tr>
<% end %>

Check out the image_tag. For each image, it's identifying the first small thumbnail then chaining that to retrieve its physical file location. It's not exactly what I was looking for since it requires an additional database hit for each image, but it's probably more accurate for my needs in this particular instance.

+2  A: 

The RailsGuides are always handy for this, I've spent a good amount of time there.

Check out the has_many :through assocation which allows for the Images model to join these tables and allow for additional fields related to the Image.

Thumbnail
has_many :images
has_many :users, :through => :images

Image
belongs_to :user
belongs_to :thumbnail

User
has_many :images
has_many :thumbnails, :through => :images

Then when accessing the Thumbnails related to the User you could do;

@user.thumbnails

Or maybe a Thumbnail should have one Image and one User?

Thumbnail
belongs_to :image

Image
belongs_to :user
has_one :thumbnail

User
has_many :images
has_many :thumbnails, :through => :images

Here's the example I gleaned this from.

revgum
Hmmm, okay. I read about :through, but I guess I read too much into what I was seeing because I equated this option with has_and_belongs_to_many. I'll reread for other associations. I still don't quite see how to get only what I need. Essentially, I'm looking for a WHERE clause that looks like "WHERE thumbnails.name = 'small' AND Thumbnail.Image.user_id = current_user" (pseudo-code, of course). Simple SQL, but I don't see a way to do it in Rails without either multiple queries or getting back a lot more than I need/want. Am I missing something?
Rob Wilkerson
Like Maulin listed below, you can add :conditions to narrow your results:@user = User.find(:id, :include => :thumbnails, :conditions => ['thumbnails.name= ?, 'small']
revgum
Oh, a point of clarification, when using :include with the User.find you're getting the join automagically because the models have associations specified. So the user_id = current_user happens as a matter of course, and the :conditions is what adds that thumbnails.name='small' clause. When you traverse the @user.thumbnails array it will be an element per row of data made by that join and clause.
revgum
I was just commenting on Twitter that there's nothing like learning a new framework to make you feel like a complete idiot. That's where I am now. What I really want to do is find the thumbnails (Thumbnail.find()) whose name is 'Small' and whose image's user_id is 1. Would you mind indulging me with code? I guess I need an example, b/c I'm not getting it at all. Doing a User.find() just doesn't make sense in my context.
Rob Wilkerson
So of course, the very next thing I tried actually worked: Thumbnail.find( :all, :include => :image, :conditions => [ "images.user_id = ? AND thumbnails.name = ?", 1, 'Small' ] ). To use associations, are you _forced_ to use string conditions?
Rob Wilkerson
No you can use boolean and integer conditions as well!
revgum
A: 

for a particular user you can find all the thumbnails by doing something like this

@user = User.find(:id, :include => :thumbnails)

:thumbnails must be defined in the user model as an association like revgum has shown in his answer.

Maulin
don't you need :include=> {:images=>:thumbnails} ?
klochner
A: 

To write complex queries beautifully you could also use something like squirrel. Squirrel is a plugin that allows you to write complex queries in a more natural fashion:

User.find(:all) do
  first_name == "Jon"         # first_name = 'Jon'
  email =~ "%@thoughtbot.com" # email LIKE '%@thoughtbot.com'
  created_at >= 4.years.ago   # created_at >= '2004-06-27 10:34:22'
  awesomeness <=> (1..10)     # awesomeness BETWEEN 1 AND 10
  banned_since == nil         # banned_since IS NULL
end

You can even make use of associations (which is what you need):

Post.find(:all) do
  user.first_name.contains? "Jon" 
end

More information at Squirrel - Natural Looking Queries for Rails. Also, be sure to check out their wiki page on Github.

Mihai Alexandru Bîrsan