views:

76

answers:

2

I am a ruby on rails newbie and had a question about the view logic in case of associated objects.

My models look similar to

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

And what I want to display is something like a list of all the posts and the 1st 3 comments for each.

So, I kept the post contoller index action simple

class PostController < ApplicationController
  #..
  def index 
    @posts = Post.find(:all)
  end
  #..
end

Now in the views/posts/index.html.erb I can do something like this @posts.comments which I can loop for the 1st 3 entries. But How do I access functionality that is normally done in the model (in this case the associated model) like ordering, scoping etc. in the view (Or controller??)?

+1  A: 

You should avoid writing complex business login in the view. In this case, your execution is simple enough that you can write all the code in your view. It should look like this

<% @posts.each do |post| %>
  <% @post.comments.all(:limit => 3, :order => "created_at DESC").each do |comment| %>
      do something
  <% end %>
<% end %>

There are a couple of possibile improvements. First, use a named_scope.

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to  :post
  named_scope :recent, proc { |limit| :limit => limit || 3, :order => "created_at DESC") }
  # you can also hard-code the limit value (ScottD)
  named_scope :recent, :limit => 3, :order => "created_at DESC"
end


<% @posts.each do |post| %>
  <% @post.comments.recent.each do |comment| %>
      do something
  <% end %>
<% end %>

If I'm right, the .each can be removed.

<% @posts.each do |post| %>
  <% @post.comments.recent do |comment| %>
      do something
  <% end %>
<% end %>

If you prefer, you can also define a custom relationship (this is for really complex relationships).

Simone Carletti
I would make some small changes to your example:1) You don't need the proc on the named scope. Just do named_scope :recent :limit => 3, :order...2) In the view I would use a partial:<%= render :partial "comment", :collection => @post.comments.recent %>Rails will send the array returned by the named_scope and render the partial in a loop for you.
ScottD
Thanks ScottD! I've updated the example.
Simone Carletti
Thanks weppos and ScottD. Got it running perfectly. Just a newbie clarification needed. In the above code when the PostContoller loads @posts and the view/partail runs @post.comments.recent when are the sql calls being fired? Is there any way I can see this (traces etc.)?
mataal
The SQL is being executed by the full @post.comments.recent statement. The translation could be "call the recent comments associated to @post object". You can view the full SQL in you log/development.log file.
Simone Carletti
+1  A: 

You could use a find method on the association which specified a limit like:

@post.comments.find(:all, :limit => 3)

in your view, or you can create another association in your Post model something like:

has_many :first_three_comments, :limit => 3, :class_name => "Comment"

and then you can just reference that association like

@post.first_three_comments.each do |comment| ...

Hope that helps.

Corban Brook
will the extra association be faster/more performant than the named_scope solution mentioned in answer http://stackoverflow.com/questions/1028830/displaying-associated-objects/1028873#1028873 ?
mataal
named scopes are a more elegant solution and supercede my solution.named_scope :cheap, :conditions => { :price => 0..5 } named_scope :recent, lambda { |*args| {:conditions => ["released_at > ?", (args.first || 2.weeks.ago)]} } named_scope :visible, :include => :category, :conditions => { 'categories.hidden' => false }http://railscasts.com/episodes/108-named-scope
Corban Brook