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.