Scopes is one of the features in Rails that I only learned about through reading The Rails 3 Way. It doesn’t seem to be a feature that’s used very often, but I think it can make your code a lot neater if it’s used correctly.
I’m going to illustrate how scopes can be used with a simple example.
A Product Application
I’ve created a very simple application to manage products. All the basic functionality is there – view/create/edit/delete.
The controller code for viewing the list of products is very simple.
def index @products = Product.all end
You’ll notice (in the screenshot) that each product has a status – ‘Available’ or ‘Coming Soon’. Let’s imagine that the user wants to be able to view all Available products. How might we accomplish that?
The easiest way to do this would be to add a separate route and action which simply filters the products to only those with the correct status. First, let’s add the route.
get 'products/available' => 'products#available', :as => 'available_products'
Now we only need to add the controller action.
def available @products = Product.where(:status => 'Available') render 'index' end
This certainly works, but there’s a cleaner way of doing this.
Using a named scope
Instead of using the where method in the controller, we can move the behavior to the model. We could create a class method to expose this filter, but Rails has a much nicer way – named scopes. Let’s see what that looks like.
class Product < ActiveRecord::Base scope :available, where(:status => 'Available') end
Now our controller code is much cleaner and easier to read.
def available @products = Product.available render 'index' end
And everything still works exactly as before. Hurrah!
Using default scope
As I mentioned, we also have the functionality to delete a product. However, we might not want to delete the record out of the database – we simply want to mark it as deleted. (There are a number of reasons why we might choose to do this – for example, we might have transactions linked to a product)
Here’s the code for deleting a product.
def destroy @product = Product.find(params[:id]) @product.update_attribute(:deleted, true) redirect_to products_path end
Let’s take another look at our list of products.
The deleted products are showing up in our regular list! While this makes sense (since we’re simply flagging them as deleted – not actually deleting them), this is definitely not what we want. So how do we fix that? One way would be to update all our queries to include :deleted => false. As you probably guessed, Rails has a neater way of doing this – using default scope.
class Product < ActiveRecord::Base default_scope where(:deleted => false) scope :available, where(:status => 'Available') end
As the name implies, default_scope changes the default list of products being returned. Our list of products no longer shows deleted products. For example, Product.all will exclude deleted products. Pretty neat.
What if we want to bypass the default scope and actually get all products? For example, I might want to have an admin function that lists all unavailable products – meaning products which have their status set to something other than Available. We can do that by using the unscoped method.
def unavailable @products = Product.unscoped.where('status <> ?', 'Available') render 'index' end
This is one of those pieces of functionality that you probably won’t miss until you know it’s around. It can definitely clean up your code and make it a bit more readable. I would probably be wary of using default_scope, but in certain scenarios it probably makes sense.
If you would like to play around with this code you can find it on Github. Happy coding.