Neu: Das englische Ruby on Rails 4.0 Buch.

6.6. resources

resources liefert Routen für eine RESTful Resource. Probieren wir das anhand der Mini-Blog-Applikation aus:
MacBook:~ xyz$ rails new blog
[...]
MacBook:~ xyz$ cd blog 
MacBook:blog xyz$ rails generate scaffold Post subject content published_at:date
[...]
MacBook:blog xyz$ rake db:migrate
[...]
MacBook:blog xyz$
Der Scaffold Generator legt automatisch eine resources Route in der config/routes.rb an:
Blog::Application.routes.draw do
  resources :posts
end

Anmerkung

Neue Routen werden von rails generate Skripten immer am Anfang von config/routes.rb hinzugefügt.
Die daraus resultierenden Routen:
MacBook:blog xyz$ rake routes
    posts GET    /posts(.:format)          posts#index
          POST   /posts(.:format)          posts#create
 new_post GET    /posts/new(.:format)      posts#new
edit_post GET    /posts/:id/edit(.:format) posts#edit
     post GET    /posts/:id(.:format)      posts#show
          PUT    /posts/:id(.:format)      posts#update
          DELETE /posts/:id(.:format)      posts#destroy
MacBook:blog xyz$
Diese RESTful Routen sind Ihnen bereits aus Kapitel 5, Scaffolding und REST bekannt. Sie werden benötigt, um die Datensätze anzuzeigen und zu verändern.

Bestimmte Routen mit :only oder :except auswählen

Wenn Sie nur bestimmte Routen aus dem fertigen Satz von RESTful Routen benutzen wollen, so können Sie diese mit :only oder :except einschränken.
Folgende conf/routes.rb definiert nur die Routen für index und show:
Blog::Application.routes.draw do

  resources :posts, :only => [:index, :show]

end
Mit rake routes können wir das Ergebnis überprüfen:
MacBook:blog xyz$ rake routes
posts GET /posts(.:format)     posts#index
 post GET /posts/:id(.:format) posts#show
MacBook:blog xyz$
except arbeitet genau anders herum:
Blog::Application.routes.draw do

  resources :posts, :except => [:index, :show]

end
Jetzt sind alle Routen bis auf index und show möglich:
MacBook:blog xyz$ rake routes
    posts POST   /posts(.:format)          posts#create
 new_post GET    /posts/new(.:format)      posts#new
edit_post GET    /posts/:id/edit(.:format) posts#edit
     post PUT    /posts/:id(.:format)      posts#update
          DELETE /posts/:id(.:format)      posts#destroy
MacBook:blog xyz$

Warnung

Denken Sie bei der Verwendung von only und except bitte daran, auch die vom Scaffold Generator generierten Views anzupassen. Dort wird z. B. auf der index-Seite mit <%= link_to 'New Post', new_post_path %> auf den new View verlinkt, der im obigen only-Beispiel dann aber gar nicht mehr existiert.

Nested Resources

Verschachtelte Ressourcen (nested resources) beziehen sich auf Routen von Ressourcen, die mit einer has_many-Verknüpfung (siehe Abschnitt 4.8, „has_many – 1:n-Verknüpfung“) arbeiten. Diese lassen sich eindeutig über Routen ansprechen. Legen wir eine zweite Ressource comment an:
MacBook:blog xyz$ rails generate scaffold comment post_id:integer content
[...]
MacBook:blog xyz$ rake db:migrate
[...]
MacBook:blog xyz$
Jetzt verküpfen wir beide Ressourcen miteinander. In der Datei app/models/post.rb fügen wir ein has_many hinzu:
class Post < ActiveRecord::Base
  attr_accessible :content, :published_at, :subject

  has_many :comments
end
Und in der Datei app/models/comment.rb das Gegenstück belongs_to:
class Comment < ActiveRecord::Base
  attr_accessible :content, :post_id

  belongs_to :post
end
Die vom Scaffold Generator erstellten Routen sehen folgendermaßen aus:
MacBook:blog xyz$ rake routes
    comments GET    /comments(.:format)          comments#index
             POST   /comments(.:format)          comments#create
 new_comment GET    /comments/new(.:format)      comments#new
edit_comment GET    /comments/:id/edit(.:format) comments#edit
     comment GET    /comments/:id(.:format)      comments#show
             PUT    /comments/:id(.:format)      comments#update
             DELETE /comments/:id(.:format)      comments#destroy
       posts GET    /posts(.:format)             posts#index
             POST   /posts(.:format)             posts#create
    new_post GET    /posts/new(.:format)         posts#new
   edit_post GET    /posts/:id/edit(.:format)    posts#edit
        post GET    /posts/:id(.:format)         posts#show
             PUT    /posts/:id(.:format)         posts#update
             DELETE /posts/:id(.:format)         posts#destroy
MacBook:blog xyz$
Wir können also mit /posts/1 das erste Post und mit /comments alle Comments abfragen. Mithilfe von Nesting können wir mit /posts/1/comments alle Comments des Posts mit der ID 1 abfragen. Dazu müssen wir die config/routes.rb abändern:
Blog::Application.routes.draw do

  resources :posts do
    resources :comments
  end

end
Damit bekommen wir dann die gewünschten Routen:
MacBook:blog xyz$ rake routes
    post_comments GET    /posts/:post_id/comments(.:format)          comments#index
                  POST   /posts/:post_id/comments(.:format)          comments#create
 new_post_comment GET    /posts/:post_id/comments/new(.:format)      comments#new
edit_post_comment GET    /posts/:post_id/comments/:id/edit(.:format) comments#edit
     post_comment GET    /posts/:post_id/comments/:id(.:format)      comments#show
                  PUT    /posts/:post_id/comments/:id(.:format)      comments#update
                  DELETE /posts/:post_id/comments/:id(.:format)      comments#destroy
            posts GET    /posts(.:format)                            posts#index
                  POST   /posts(.:format)                            posts#create
         new_post GET    /posts/new(.:format)                        posts#new
        edit_post GET    /posts/:id/edit(.:format)                   posts#edit
             post GET    /posts/:id(.:format)                        posts#show
                  PUT    /posts/:id(.:format)                        posts#update
                  DELETE /posts/:id(.:format)                        posts#destroy
MacBook:blog xyz$ 
Allerdings müssen wir in app/controllers/comments_controller.rb noch ein paar Änderungen vornehmen. Damit stellen wir sicher, dass immer nur die Comments des angegebenen Posts angezeigt oder verändert werden (um die Übersicht zu verbessern, habe ich den JSON-Teil herausgelöscht):
class CommentsController < ApplicationController
  before_filter :find_post

  def index
    @comments = @post.comments
  end

  def show
    @comment = @post.comments.find(params[:id])
  end

  def new
    @comment = @post.comments.build
  end

  def edit
    @comment = @post.comments.find(params[:id])
  end

  def create
    @comment = @post.comments.build(params[:comment])

    if @comment.save
      redirect_to [@post, @comment], notice: 'Comment was successfully created.'
    else
      render action: "new"
    end
  end

  def update
    @comment = @post.comments.find(params[:id])

    if @comment.update_attributes(params[:comment])
      redirect_to [@post, @comment], notice: 'Comment was successfully updated.'
    else
      render action: "edit"
    end
  end

  def destroy
    @comment = @post.comments.find(params[:id])
    @comment.destroy

    redirect_to post_comments_path(@post)
  end

  private
  def find_post
    @post = Post.find(params[:post_id])
  end
end
Leider ist das nur die halbe Miete, da in den Views noch auf die alten Routen verwiesen wird. Wir müssen also jeden View entsprechend der Nested Route anpassen.
app/views/comments/_form.html.erb
Bitte beachten Sie hier, dass der form_for-Aufruf auf form_for([@post, @comment]) geändert werden muss.
<%= form_for([@post, @comment]) do |f| %>
  <% if @comment.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:</h2>

      <ul>
      <% @comment.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :content %><br />
    <%= f.text_field :content %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
app/views/comments/edit.html.erb
<h1>Editing comment</h1>

<%= render 'form' %>
app/views/comments/index.html.erb
<h1>Listing comments</h1>

<table>
  <tr>
    <th>Post</th>
    <th>Content</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @comments.each do |comment| %>
  <tr>
    <td><%= comment.post_id %></td>
    <td><%= comment.content %></td>
    <td><%= link_to 'Show', [@post, comment] %></td>
    <td><%= link_to 'Edit', edit_post_comment_path(@post, comment) %></td>
    <td><%= link_to 'Destroy', [@post, comment], confirm: 'Are you sure?', method: :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New Comment', new_post_comment_path(@post) %>
app/views/comments/new.html.erb
<h1>New comment</h1>

<%= render 'form' %>
app/views/comments/show.html.erb
<p id="notice"><%= notice %></p>

<p>
  <b>Post:</b>
  <%= @comment.post_id %>
</p>

<p>
  <b>Content:</b>
  <%= @comment.content %>
</p>
Bitte spielen Sie einmal mit den unter rake routes aufgeführten URLs herum. So können Sie jetzt mit /posts/new einen neuen Post und mit /posts/:post_id/comments/new einen neuen Comment zu diesem Post generieren.

Bemerkungen zu Nested Resources

Im Allgemeinen sollte man nie tiefer als eine Ebene "nesten" und Nested Resources sollten sich "natürlich" anfühlen. Sie werden mit der Zeit ein Gefühl dafür bekommen. Meiner Meinung nach ist das Wichtigste an RESTful Routen, dass sie sich logisch anfühlen müssen. Wenn Sie mit einem befreundeten Rails-Programmierer telefonieren und ihm sagen "Ich habe da eine Resource Post und eine Resource Comment", dann sollte beiden Seiten direkt klar sein, wie man diese Resourcen per REST anspricht und wie man sie verschachteln kann.

Autor

Stefan Wintermeyer