Rails 3 nested resources short name?

2019-03-27 16:03发布

问题:

I'm in the process of upgrading a Rails 2.3 app to Rails 3. In the Rails 2.3 router, it was possible to set a :name_prefix of nil on nested resources to get a shorter name. The actual URL would still be fully qualified, but the code could use a shorter name. E.g.,:

 map.resources :sites do |site|
    site.resources :groups, :as => :groups, :controller => :url_groups, :name_prefix => nil, :member => { :clone => :post } do |group|
      group.resources :tests, :as => :tests, :controller => :test_runs, :name_prefix => nil, :collection => { :latest => :get }
    end
  end

would allow one to use latest_tests_path. I can't figure out how to do the same thing with Rails 3, so I'm stuck with latest_site_group_tests_path. If that's the way it needs to be, I can just go through the code and change every instance of it. But I wanted to make sure I didn't miss anything first. And for better or worse, I do need to maintain the URL structure, so shallow routes don't seem to be the answer.

回答1:

Good news is that Rails 3 still has the ability to setup arbitrary/abbreviated url helpers. Rather than a parameter to the resources method, you can create short-hand url helpers with the match declaration in routes.rb.

Say we have routes setup like this (noting that you need to maintain the 3 levels of nesting):

resources :sites do
  resources :groups, :controller => :url_groups do
    member do
      post :clone
    end
    resources :test_runs do
      collection do
        get :latest
      end
    end
  end
end

We get all the standard url helpers (rake routes):

           clone_site_group POST   /sites/:site_id/groups/:id/clone(.:format)                    {:action=>"clone", :controller=>"url_groups"}
latest_site_group_test_runs GET    /sites/:site_id/groups/:group_id/test_runs/latest(.:format)   {:action=>"latest", :controller=>"test_runs"}
       site_group_test_runs GET    /sites/:site_id/groups/:group_id/test_runs(.:format)          {:action=>"index", :controller=>"test_runs"}
                           (etc)

But to create something shorter than latest_site_group_test_runs_path(site,group), add a match declaration to routes.rb like this:

match 'sites/:site_id/groups/:id/test_runs/latest' => 'test_runs#latest', :as => :latest_tests

Now you can use latest_tests_path(site,group) or latest_tests_url(site,group) to generate the fully nested path.

If your aim is brevity, you could also use implicit polymorphic paths (as long as you have all your models aligned with resource paths).

For example, given @site #1 and @group #1, all of the following will now generate the same path '/sites/1/groups/1/test_runs/latest':

= link_to "latest tests", latest_site_group_test_runs_path(@site,@group) # std helper
= link_to "latest tests", latest_tests_path(@site,@group) # match helper
= link_to "latest tests", [:latest,@site,@group,:test_runs] # implicit polymorphic path

Hope that helps! Seems like you should be able to get the flexibility you need for the app migration.

NB: I glossed over the lurking issue of having a model called "Test" since that's off topic;-) There are a few model names that are a neverending source of pain because of namespace and keyword conflicts. My other favourite is when I really wanted to have a mode called "Case" (since that matched the problem domain best. Bad idea, rapidly reversed!)



回答2:

There's the :shallow option (see the documentation), but I'm not sure it fits your use case:

resources :sites, :shallow => true
  resources :groups do
    resources :tests
  end
end

It has the disadvantage of creating a bunch of additional routes:

   group_tests GET    /groups/:group_id/tests(.:format)        {:action=>"index", :controller=>"tests"}
               POST   /groups/:group_id/tests(.:format)        {:action=>"create", :controller=>"tests"}
new_group_test GET    /groups/:group_id/tests/creer(.:format)  {:action=>"new", :controller=>"tests"}
     edit_test GET    /tests/:id/modifier(.:format)            {:action=>"edit", :controller=>"tests"}
          test GET    /tests/:id(.:format)                     {:action=>"show", :controller=>"tests"}
               PUT    /tests/:id(.:format)                     {:action=>"update", :controller=>"tests"}
               DELETE /tests/:id(.:format)                     {:action=>"destroy", :controller=>"tests"}
   site_groups GET    /sites/:site_id/groups(.:format)         {:action=>"index", :controller=>"groups"}
               POST   /sites/:site_id/groups(.:format)         {:action=>"create", :controller=>"groups"}
new_site_group GET    /sites/:site_id/groups/creer(.:format)   {:action=>"new", :controller=>"groups"}
    edit_group GET    /groups/:id/modifier(.:format)           {:action=>"edit", :controller=>"groups"}
         group GET    /groups/:id(.:format)                    {:action=>"show", :controller=>"groups"}
               PUT    /groups/:id(.:format)                    {:action=>"update", :controller=>"groups"}
               DELETE /groups/:id(.:format)                    {:action=>"destroy", :controller=>"groups"}
         sites GET    /sites(.:format)                         {:action=>"index", :controller=>"sites"}
               POST   /sites(.:format)                         {:action=>"create", :controller=>"sites"}
      new_site GET    /sites/creer(.:format)                   {:action=>"new", :controller=>"sites"}
     edit_site GET    /sites/:id/modifier(.:format)            {:action=>"edit", :controller=>"sites"}
          site GET    /sites/:id(.:format)                     {:action=>"show", :controller=>"sites"}
               PUT    /sites/:id(.:format)                     {:action=>"update", :controller=>"sites"}
               DELETE /sites/:id(.:format)                     {:action=>"destroy", :controller=>"sites"}

Besides, the shallow nesting only applies to the following routes: :show, :edit, :update, :destroy.



回答3:

I wanted something like this too, clearly it feels redundant to type the prefixes in named routes all the time for resources that only exist as nested. I was able to get what I wanted using scope, I think this is what you sought too:

resources :sites

scope :path => '/sites/:site_id' do
  resources :groups, :controller => :url_groups do
    post :clone, :on => :member
  end
end

scope :path => '/sites/:site_id/groups/:group_id' do
  resources :tests, :controller => :test_runs do
    get :latest, :on => :collection
  end
end

The rake routes output:

       sites GET        /sites(.:format)                                             sites#index
             POST       /sites(.:format)                                             sites#create
    new_site GET        /sites/new(.:format)                                         sites#new
   edit_site GET        /sites/:id/edit(.:format)                                    sites#edit
        site GET        /sites/:id(.:format)                                         sites#show
             PUT        /sites/:id(.:format)                                         sites#update
             DELETE     /sites/:id(.:format)                                         sites#destroy
 clone_group POST       /sites/:site_id/groups/:id/clone(.:format)                   url_groups#clone
      groups GET        /sites/:site_id/groups(.:format)                             url_groups#index
             POST       /sites/:site_id/groups(.:format)                             url_groups#create
   new_group GET        /sites/:site_id/groups/new(.:format)                         url_groups#new
  edit_group GET        /sites/:site_id/groups/:id/edit(.:format)                    url_groups#edit
       group GET        /sites/:site_id/groups/:id(.:format)                         url_groups#show
             PUT        /sites/:site_id/groups/:id(.:format)                         url_groups#update
             DELETE     /sites/:site_id/groups/:id(.:format)                         url_groups#destroy
latest_tests GET        /sites/:site_id/groups/:group_id/tests/latest(.:format)      test_runs#latest
       tests GET        /sites/:site_id/groups/:group_id/tests(.:format)             test_runs#index
             POST       /sites/:site_id/groups/:group_id/tests(.:format)             test_runs#create
    new_test GET        /sites/:site_id/groups/:group_id/tests/new(.:format)         test_runs#new
   edit_test GET        /sites/:site_id/groups/:group_id/tests/:id/edit(.:format)    test_runs#edit
        test GET        /sites/:site_id/groups/:group_id/tests/:id(.:format)         test_runs#show
             PUT        /sites/:site_id/groups/:group_id/tests/:id(.:format)         test_runs#update
             DELETE     /sites/:site_id/groups/:group_id/tests/:id(.:format)         test_runs#destroy

Update: Duh, I left the static segments of the scope paths out of my example. Fixed.