Regulators!!! Mount up

Posted by Matt Jankowski

Jul 10

In my constant quest to REST up web apps and create a better world for our children and our children’s children, I’ve started to use “nested” controllers to create a local namespace/hierarchy in the controllers of our Rails apps. The pattern here is simple. Let’s say you have a User model and an Article model. User has_many articles. You need to have both a global “all articles” page and a user-specific “articles by this user” page.

Clear black night

Your routes will look like something like this…

1
2
map.resources :users, :has_many => :articles
map.resources :articles

Then in articles_controller you might have an #index action like…

1
2
3
4
5
6
7
8
9
class ArticlesController < ActionController::Base
  def index
    if params[:user_id].blank?
      @articles = Article.paginate :all, :page => params[:page]
    else
      @articles = current_user.articles.paginate :all, :page => params[:page]
    end
  end
end

Clear white moon

Now, that’s not that bad, there’s only one conditional, but this is a pretty simple example. Using the nested approach to create a namespace, we would do this in the routes…

1
2
map.resources :users { |users| users.resources :articles, :controller => 'users/articles' }
map.resources :articles

The :controller option is going to tell all of “users/:user_id/articles/....” routes which previously went to the top-level ArticlesController (in app/controllers/articles_controller.rb) to go to the Users::ArticlesController (which is found in app/controllers/users/articles_controller.rb) instead. So now to handle the global and user article listings we’ll have two controllers, each with an index action.

1
2
3
4
5
6
7
8
9
10
11
class ArticlesController < ActionController::Base
  def index
    @articles = Article.paginate :all, :page => params[:page]
  end
end

class Users::ArticlesController < ActionController::Base
  def index
    @articles = current_user.articles.paginate :all, :page => params[:page]
  end
end

You could argue that this ends up being more lines of code (you would be right, barely!) and that it ends up being more files (you are completely right!), and I won’t disagree. I’m comfortable with both of those things, and I think the gain you get from moving the conditional logic to the routes and out of the controller (not to mention the corresponding simplification of the tests) makes it worth it. Also, this is a very simple example which only has one action, and one level of nesting, and one modeled association. I’ve found that the benefit gained from this pattern only goes up as the scenario gets more and more complex.

Handy with the steel

Now, to the actual point of this blog post! If you are embracing the above pattern, and you have singular resources, make sure that you do not use a singular resource name that is the same as any model you have. I was recently applying this pattern to an application which has User, Blog and Post models. User has_one blog. Blog has_many posts. Post belongs_to blog. I attempted to use routes like:

1
2
3
4
5
6
7
8
9
10
11
12
map.resources :users, :as => :people do |users|
  users.resource :blog, :controller => 'users/blog' do |blog|
    blog.resources :posts, :controller => 'users/blog/posts', :collection => { :list => :get } do |posts|
      posts.resources :comments, :controller => 'users/blog/posts/comments'
      posts.resource :tags, :controller => 'users/blog/posts/tags'
    end
  end
end

# Creates routes like:
# PUT /people/:user_id/blog/posts/:post_id/tags => Users::Blog::Posts::TagsController#update
# GET /people/:user_id/blog => Users::BlogController#show

After this was in place, and controllers like Users::Blog::Posts (in app/controller/users/blog/posts_controller.rb) existed, things seemed to be going according to plan, but then the development team started to get different errors, on different machines, some of the time. Actually, that’s not giving it enough credit – many of the errors were consistently repeatable on one person’s machine but not at all on someone else’s (we attempted to resolve by comparing ruby versions and so on, but never arrived at a conclusion). We encountered four separate problems:

213 will regulate

So, in this particular example the issue was that since we already had a “Blog” model class, the implicit “Users::Blog::Posts” module that was created by that controller was colliding with that top level Blog constant from the model. The solution was pretty simple – we just had to rename the “Blog” in there to “Blogs”, and adjust the corresponding controllers. The general rule to take from this is “dont use modules to create namespaces in controllers that are also names of pre-existing constants from other class definitions”. This is probably the largest hiccup I’ve found using this approach, and once we actually understand what was happening, it was a pretty straightforward fix.

We’re using this pattern in an application with 107 controller classes that have ~350 actions between them – and having the controllers organized into separate directories on the filesystem and separate namespace/hierarchy in the codebase definitely makes the application more manageable.


Comments on this post

Bo.

Jul 10

Bo. said,

Just one question: when I’m trying to watch the video you linked, it tells me “This video is not available in your country.” I’m currently live in Ukraine, though my “Location Info” at youtube set to United Kingdom. If it’s your video up there, can you, please, clarify, why is it unavailable here?

Josh Nichols

Jul 10

Josh Nichols said,

What, no embedded video clip? It’s what I’ve come to expect from posts on Giant Robots!

Matt Jankowski

Jul 10

Matt Jankowski said,

Apparently Universal Music Group requested that youtube disable embedding for that video. In retrospect, I should have picked another video about regulating which was embeddable and not subject to international censorship.

Dan Croak

Jul 10

Dan Croak said,

Matt,

Here’s a fascinating video wherein Senator Hillary Rodham Clinton took her continuing concerns about safety and security at Indian Point directly to officials from the Nuclear Regulatory Commission.

josh

Jul 10

josh said,

uncanny timing with this post. I’m building a rails app with REST and nested resources. I’m a little stuck with what to do with a resource that doesn’t present good user interaction when implemented RESTfully. ..bookmarking this for later.

Luis Lavena

Jul 10

Luis Lavena said,

Nice post Matt,

One question: is current_user the user in context (from :user_id)

Or is a typo in your post? Because if some user different than me log in and try to see my blog… will not get mine but his instead.

Just a thought, maybe I’m wrong (which is true most of the time).

Regards, Luis

Matt Jankowski

Jul 10

Matt Jankowski said,

@josh – feel free to post about any weird modeling requirements. Even if I don’t have a good solution for you, I’ll offer you a totally contrived one and tell you it’s good.

@Luis – yes, in that first example #current_user would be “the user found by looking at params[:user_id]” and not “the currently logged in session user”.

Don

Jul 10

Don said,

From the post title, I was really hoping you were going to do some king of Ruby/Young Guns analogy. Ah well.

Arthur Schreiber

Jul 10

Arthur Schreiber said,

You don’t have to specify the :controller if you do use the block form (at least in Rails 2.0):

1
2
3
4

map.resources :users, :as => :people do |users|
  users.resource :blog, :controller => 'users/blog'
end

should be the same as:

1
2
3
4

map.resources :users, :as => :people do |users|
  users.resource :blog
end

That’s a lot cleaner imho.

Matt Jankowski

Jul 10

Matt Jankowski said,

@Arthur – I’m not sure that’s correct. The “users.resources :blog” that you have there will route to the top level BlogsController by default in rails 2.0 and 2.1. It will not go to Users::BlogsController, which is what I’m trying to accomplish.

There is a “map.namespace” which is what you may be thinking of?

Adam

Jul 10

Adam said,

@arthur – I agree with Matt that nested resources don’t automatically get nested controller names like Users::BlogsController

@Matt – what you can use is the :namespace attribute in the map.resource(s) like the following:

1
2
3
4

map.resources :users, :as => :people, :namespace => 'users/' do |users|
  users.resource :blog
end

This will give you the same effect you are looking for. Its not the cleanest but its a little better than specifying the full controller name everytime.

One gotcha is that nested namespaces don’t nest but rather override.

Arthur Schreiber

Jul 11

Arthur Schreiber said,

Oh, yes, you guys are right, sorry. But the following should have the same effect:

1
2
3
4

map.resources :users, :as => :people do |users|
  users.resource :blog, :namespace => "users/"
end
Julien

Jul 15

Julien said,

I’ve got some behavior in ArticlesController that I would love to keep into Users:: ArticlesController : filters for example… would it be a good idea to have Users:: ArticlesController inheriting from ArticlesController, like this :

1
2
3
4
5
6
7
8
9
10
11
12

class ArticlesController < ActionController::Base
  def index
    @articles = Article.paginate :all, :page => params[:page]
  end
end

class Users::ArticlesController < ArticlesController
  def index
    @articles = current_user.articles.paginate :all, :page => params[:page]
  end
end
Julien

Jul 15

Julien said,

Oups, second question : what should I do if the behavior is the same in both Controllers (ArticlesController & Users:: ArticlesController) for a given action?

For example, if the index action is likely to be different in your case, I guess that a show action would display the article the same way, wether the User is specified of not.

If inheritance is a good idea (see my previous comment), then we can just “forget” to overide this action… but, this will bring the problem of “duplicate content” and that’s not restful anymore : the same resource has 2 different URL.

My idea would then to do a basic redirect_to , what do you think?

Matt Jankowski

Jul 15

Matt Jankowski said,

Julien – if you had a very large number of filters in the controller, it might make sense to keep it DRY by inheriting the nested one from the other one – however, this is somewhat unexpected behavior, so I’d be sure to comment/document this near the top of the inherited class file so that future developers understand why it’s happening (and, obviously, to thoroughly test both controllers).

In this particular example I think the case I was thinking of would only have the show action, and no other actions. And yes, I would avoid creating more urls for what was ultimately the same thing, and just use the “higher” one in the top level controller for the actual “permalink” for that resource.

Julien

Jul 15

Julien said,

Thanks for all this!

When you say, you would avoid creating more urls, how would you make this if these are the REST default routes? Can you remove some the defaults routes (for example remove /resource/1/edit while keeping show (/resource/1) for example?

Matt Jankowski

Jul 15

Matt Jankowski said,

The routes would exist, I would just not link to them.


Sorry, comments are closed for this article.

© 2000 - 2009 by thoughtbot, inc.
written by a bushel of tiny robots