Emulate Default Scope with Around Filter and Scoping

We all know that default_scope is evil. But sometimes, you really do want to make sure that a condition is almost always applied to the queries for a particular model. Draft vs. published posts, approved vs. unapproved content, soft deletes, cat videos vs. not-cat videos, etc., etc. What’s a well-behaved Rails developer to do? Recently, I encountered this exact issue on a project I was working on, and was not satisfied with the choice between Being Evil à la default_scope or being repetitive and scattering where(whatever: true) conditions throughout the code. After some searching, I came across a combination of methods that accomplishes essentially the same goal without all of the bizarre side effects of default_scope.

Le Hypothetical Situation

Imagine that we want to build a blogging platform in Rails with the usual draft-before-publish workflow. Basically, we want to make sure that drafts only ever show up when an admin is logged in. The first solution that comes to mind for a problem like this is to chuck the condition into a default_scope on your model:

class Post < ActiveRecord::Base
          default_scope where(draft: false)
        end
        

I won’t rehash the challenges of using default_scope in this post; give the Rails Best Practices link at the top of this post a good read and you should at least get the general idea that there are a lot of weird things to account for with this approach.

Le Alternative Different

Enter after_filter and scoping. Below is a simple example of using this approach in the ApplicationController to accomplish the same draft filtering:

class ApplicationController < ActionController::Base
          protect_from_forgery with: :exception
        
          around_filter :scope_to_published
        
          private
        
          def scope_to_published
            Post.where(draft: false).scoping do
              yield
            end
          end
        end
        

This results in the Post model being “globally” scoped before running the rest of the controller method. Since it happens in the ApplicationController, it will apply to all subclassed controllers. (Worth noting is that where(draft: false) can of course be converted to an equivalent scope named published or some such.) Using this approach, and some thoughtful placement in the controller hieararchy, it is possible to increase or decrease the granularity of a “default” condition. It is easy to imagine having an AdminController that does not inherit from ApplicationController and would therefore not be subject to this global scoping. Alternatively, a skip_filter directive could be added only to those controllers where this scoping should not apply. In the direction of more specificity, the around_filter could be moved further down the class hierarchy and explicitly applied to the PostsController or any number of related controllers.

Un Caveat

As a disclaimer, I have not benchmarked the effect this might have at the ApplicationController level. I would guess that in the best case, it should incur no more performance overhead than an explicit where clause, and in the worst case, it would incur a trivial impact more than made up for by conciseness. Can’t say for certain without running benchmarks, but I invite any interested readers to share any findings or insights they might have in this regard.

comments powered by Disqus