Rails 4 [Best practices] Nested resources and shallow: true
Asked Answered
M

5

29

The following post is based on Rails 4.

I am currently looking for a best-practice about the multiple nested resources (more than 1), and the option shallow: true.

Initially in my routes, there was this :

resources :projects do 
  resources :collections
end

The associated routes are :

    project_collections GET    /projects/:project_id/collections(.:format)          collections#index
                        POST   /projects/:project_id/collections(.:format)          collections#create
 new_project_collection GET    /projects/:project_id/collections/new(.:format)      collections#new
edit_project_collection GET    /projects/:project_id/collections/:id/edit(.:format) collections#edit
     project_collection GET    /projects/:project_id/collections/:id(.:format)      collections#show
                        PATCH  /projects/:project_id/collections/:id(.:format)      collections#update
                        PUT    /projects/:project_id/collections/:id(.:format)      collections#update
                        DELETE /projects/:project_id/collections/:id(.:format)      collections#destroy
               projects GET    /projects(.:format)                                  projects#index
                        POST   /projects(.:format)                                  projects#create
            new_project GET    /projects/new(.:format)                              projects#new
           edit_project GET    /projects/:id/edit(.:format)                         projects#edit
                project GET    /projects/:id(.:format)                              projects#show
                        PATCH  /projects/:id(.:format)                              projects#update
                        PUT    /projects/:id(.:format)                              projects#update
                        DELETE /projects/:id(.:format)                              projects#destroy

I read in the documentation about the limitation of nested resources :

Resources should never be nested more than 1 level deep.

Ok. Then, like the documentation said, I'm gonna use "shallow" in my routes instead.

shallow do
  resources :projects do 
    resources :collections
  end
end

The associated routes are :

   project_collections GET    /projects/:project_id/collections(.:format)     collections#index
                       POST   /projects/:project_id/collections(.:format)     collections#create
new_project_collection GET    /projects/:project_id/collections/new(.:format) collections#new
       edit_collection GET    /collections/:id/edit(.:format)                 collections#edit
            collection GET    /collections/:id(.:format)                      collections#show
                       PATCH  /collections/:id(.:format)                      collections#update
                       PUT    /collections/:id(.:format)                      collections#update
                       DELETE /collections/:id(.:format)                      collections#destroy
              projects GET    /projects(.:format)                             projects#index
                       POST   /projects(.:format)                             projects#create
           new_project GET    /projects/new(.:format)                         projects#new
          edit_project GET    /projects/:id/edit(.:format)                    projects#edit
               project GET    /projects/:id(.:format)                         projects#show
                       PATCH  /projects/:id(.:format)                         projects#update
                       PUT    /projects/:id(.:format)                         projects#update
                       DELETE /projects/:id(.:format)                         projects#destroy

The major difference I see is the "show" of collections, this specific one :

collection GET    /collections/:id(.:format)                      collections#show

So if I I'm correct, the link for the show action for a collection is :

<%= link_to 'Show", collection_path(collection)%>

and should return something like this : "http://example.com/collections/1"

BUT ! 2 things :

  • This is not working. I'm getting instead "http://example.com/projects/1".
  • Even if it was working, it's actually IMO pretty bad because I loose the REST basic that say "Collection is child of project, then the url should be "localhost/project/1/collections/1"

I don't understand what is the interest of shallow if I loose the big advantage of Rest actions. And what is the interest to loose the "Show" action as well ? I already posted this to SO, but the only comment i got is "It's something normal". I don't believe this is a normal behavior to "remove" an action from the rest API ?

Yes, it might be convenient for the helpers to use shallow, but it is NOT AT ALL convenient for the rest, you loose all the interest of "one collection is nested to one project, so this is reflected in the URL".

I don't know if there is another way to do this, it's true that shallow allow more flexibility about the helpers, but it's false that it is REST compliant. So, is there any chance to get the "helpers" working (it's pretty awesome to have "nested3_path(collection)" instead of "nested1_nested2_nested3([nested1.nested2.nested3, nested1.nested2, nested1])", and keeping the "url part "nested1/123/nested2/456/nested3/789" ?

Mcduffie answered 19/2, 2014 at 0:30 Comment(2)
Have you tried restarting the server for the routes to take effect? According to the doc resources :posts, shallow: true do resources :comments end will produce resources :posts do resources :comments, except: [:show, :edit, :update, :destroy] end resources :comments, only: [:show, :edit, :update, :destroy] which sound like you are doingMarchall
The server does indeed have to be restarted for routes to take effect.Knut
G
21

I don't believe that Rails offers any built-in way to have the URLs use the full hierarchy (e.g. /projects/1/collections/2) but also have the shortcut helpers (e.g. collection_path instead of project_collection_path).

If you really wanted to do this, you could roll out your own custom helper like the following:

def collection_path(collection)
  # every collection record should have a reference to its parent project
  project_collection_path(collection.project, collection)
end

But that would be quite cumbersome to manually do for each resource.


I think the idea behind the use of shallow routes is best summed up by the documentation:

One way to avoid deep nesting (as recommended above) is to generate the collection actions scoped under the parent, so as to get a sense of the hierarchy, but to not nest the member actions. In other words, to only build routes with the minimal amount of information to uniquely identify the resource

source: http://guides.rubyonrails.org/routing.html#shallow-nesting

So while this may not be REST-compliant (as you say), you aren't losing any information because each resource can be uniquely identified and you are able to walk back up the hierarchy assuming your associations are set up properly.

Gurias answered 25/2, 2014 at 4:41 Comment(1)
While it's not built into Rails, Inherited Resources (IR) has nesting and helpers that might help. It might be good instead to be explicit and not use IR if it is just a handful of cases in which this is done, and it also can restrict or complicate controller implementation/debugging, depending on your needs.Rangel
A
10

Since there's an id for a Collection, it's redundant to nest the route under the Project except for the index and create actions.

There's a rule about URL's where there's only supposed to be one URL to GET (with 200) a given resource, if there are other URL's you should redirect to it. So you might have a route /projects/:id/collections/:collection_id that redirects to /collections/:collection_id.

In your case, a Collection is tied to a Project, but that's not necessarily true for all relationships. Once you have the :collection_id you don't need to reference the context of the Project to access it.

Appellation answered 25/2, 2014 at 19:7 Comment(1)
Yes! Thank you! I've been googling around looking for a basic explanation of the purpose of shallow routes! Assumption in the documentation is: you already know this. Judging from some of other answers here, a lot of others don't know either! What you are saying is if you are for example editing a comment by :id, it doesn't make sense to nest that under a project, because the comment :id is independent of the project id. Shallow just cleans up the routes to reflect this.Heigho
K
2

Levels

The notion you have to only use 1 level in your nested resources is only really applicable to the design of the system:

The corresponding route helper would be publisher_magazine_photo_url, requiring you to specify objects at all three levels. Indeed, this situation is confusing enough that a popular article by Jamis Buck proposes a rule of thumb for good Rails design:

I believe Rails can still handle multiple levels, although it's not recommended from a usability perspective


Shallow

Although I've seen shallow used before, I've never used it myself

From looking at the documentation, it seems shallow has a rather obscure purpose (I don't actually know why it's there). The problem is you aren't publicly passing the post_id parameter to your controller, leaving you to load the collection without an important param

I would surmise (and this is just speculation), that the aim is to pass the param you require behind the scenes, so you're left with a public "shallow" route:

#config/routes.rb
resources :projects do 
   resources :collections, shallow: true
end

I would imagine you'd get a URL helper like this:

collection_path(project.id, collection.id)

This would come out as domain.com/collection/2

Kildare answered 19/2, 2014 at 9:54 Comment(1)
Yes you're right on the routes, but that don't help much for my question =/Mcduffie
R
2

Though it can complicate things if you only need this for some models, it might be good to check out Inherited Resources (IR). It supports resource nesting, polymorphic belongs to's, and can automatically generate the shorter path and url helper methods you are looking for. The reason you don't hear about IR much anymore is that its original author and some other developers have somewhat abandoned it because of the complications that arise when trying to extend your controllers. However, it still has a community, and we've tried to extend it a bit more and focus more on ease of controller extensions with Irie.

The "best practice" in Rails depends on who you talk to.

Rails has traditionally been aimed at mostly basic CRUD for (non-nested) resources. Yes, it allows retrieving and updating nested resources, but it is assumed that doesn't happen quite as often.

However, what has been emerging in the Rails community is the ActiveModel::Serializers/json-api approach. In this, usually not more than one level of nesting of resources occurs, and the nested resource is either a list of links or sideloaded small version of the child resources which you can then query on that resource to get more data. This has also been embraced by Ember/Ember Data.

There are also roar and a number of other projects that aim to implement something closer to their understanding of something close to Roy Fielding's original vision of REST.

I think it just depends on what your design is and what you need. If efficiency is a goal, then the additional time to develop to be explicit and nest more may pay off. We currently use AngularJS and Irie, for example. But, to each his own.

As as last note, be sure to avoid n+1 lookups through use of includes(...) (or similar) in your queries, otherwise all that nesting might bite you in performance.

Rangel answered 28/2, 2014 at 16:8 Comment(0)
K
1

From this answer it seems shallow routes somewhat defy the convention of Rails, IMO.

I would think you wouldn't need the explicit path helper for a show route. The link_to helper should be able to infer it from the object's to_param method.

#your helper becomes 
link_to "show", collection

If you use the helper your way as you have above you probably need to pass the nested ID of the parent resource to the helper too.

link_to "show", collection_path([project, collection])
Kanzu answered 2/3, 2014 at 20:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.