How to Implement Custom Scopes in Rails 5
This guide gives a step by step set of instructions for building custom database scopes in a Rails 5 application, including both of the syntax options available in Rails.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

In this guide, we're going to examine the next item on our PivotalTracker task list, which is to create custom scopes. If you've never heard this term before, it can seem a bit daunting. However it’s simply a custom database query.

We have a good example of what this means, so let's start up the rails server rails sand navigate to our portfolio items in the browser

On this page you'll see a list of portfolio items, and each has a subtitle that says “My great Service”. Let's say this service was basically a description and we could create a set of them. For example, the first could be Rails, second could be Angular etc. We can go to the rails console to examine how we can do that.

Updating the Seeds File

It has been a while since we updated our seeds file, let's do that. Open seeds.rb Because we just created them, we need to bring in the concept of topics, so this is a great time for this update. Let’s begin by creating a couple of topics. We will use .times and do. We also want to add our puts statement after the loop.

3.times do |topic|
end
puts "3 Topics created”

Inside the loop we want to create our topics. We will indicate that with a title of “Topic” and use string interpolation #{topic} to differentiate each one so it will return Topic 0, Topic 1, and Topic 2.

3.times do |topic|
  Topic.create!(
    title: "Topic #{topic}"
  )
end
puts "3 Topics created”

Because we have created a database relationship between topics and blogs, we should indicate that in our blog. At the bottom of the blog body, add a comma , and topic_id: Topic.last.id This means all of the blogs will be associated with the third topic created by our loop above. With that we can have a reference point to the relationship. We won’t use it too much right now, but eventually we will.

10.times do |blog|
  Blog.create!(
    title: "My Blog Post #{blog}",
    body: "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?",
    topic_id: Topic.last.id
  )
end

Next, we'll revise our portfolio items. We'll create 8 items that use "Ruby on Rails" as the service and one that uses "Angular". This will still create a total of 9 portfolio items.

First we need to change the number of times we loop through portfolio items

- 9.times do |portfolio_item|
+ 8.times do |portfolio_item|

Then we need to update the subtitle to indicate the new service

- subtitle: "My great Service",
+ subtitle: "Ruby on Rails",

Your portfolio item will now look like this:

8.times do |portfolio_item|
  Portfolio.create!(
    title: "Portfolio title: #{portfolio_item}",
    subtitle: "Ruby on Rails",
    body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    main_image: "http://placehold.it/600x400",
    thumb_image: "http://placehold.it/350x200"
  )
end

Copy and paste your portfolio item so there will be two loops in your file for portfolios. Then change second loop to reflect 1.times do |portfolio_item| and change the subtitle to "Angular",

1.times do |portfolio_item|
  Portfolio.create!(
    title: "Portfolio title: #{portfolio_item}",
    subtitle: "Angular",
    body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    main_image: "http://placehold.it/600x400",
    thumb_image: "http://placehold.it/350x200"
  )
end

Your seeds file will now create 8 portfolio items with Rails as a service and 1 portfolio item with Angular as a service.

With those updates in place we need to go to the terminal and run rails db:setup. This command is going to wipe out our database and create our new set of data. We can test this by opening the rails console rails s.

To begin, we will look for the subtitle on our first portfolio item by using the imperative Portfolio.first.subtitle. This returns => “Ruby on Rails” Which is exactly what we are looking for.

Next, let’s query our last portfolio item service with the code Portfolio.last.subtitle This will return => "Angular". So this is working perfectly.

large

Database Queries from the Console

We will stay here in the console so we can talk about database queries. This information will be directly connected to our discussion on scopes. When I use,
Portfolio.where(subtitle: "Ruby on Rails")
the subtitle: "Ruby on Rails” portion means where the subtitle equals Ruby on Rails. This query will bring back all (eight) of the items where 'Ruby on Rails' is the subtitle.

If you look at the SQL code that was generated when we ran the query SELECT “portfolios” FROM “portfolios” WHERE “portfolios” “subtitle” = $1 [[“subtitle”, “Ruby on Rails]]”, in other words, this says, Select the portfolios from the portfolio table where the subtitle is ‘Ruby on Rails’

If I did the same query and just added .count
Portfolio.where(subtitle: "Ruby on Rails”).count
The console will return => 8 So now you know how to run these queries in the console, let’s see how it works on the page itself. Exit the console command d, and start the rails server rails s

Database Queries in the Code

In the browser, navigate to the portfolios page. Refresh your page and take note that the first eight records have the subtitle "Ruby on Rails", while the last one has “Angular". We could change the subtitle to whatever we want.

Open the portfolios_controller.rb file. By default, our index action is bringing up all .all the portfolio items, which is fine. However, we can customize this by using .where

class PortfoliosController < ApplicationController
  def index
    @portfolio_items = Portfolio.where(subtitile: ‘Ruby on Rails’)
  end

Save the file. Now if you refresh the browser page, you'll see that the item with the "Angular" subtitle is not displayed. Conversely, if you change the query to "Angular",

- @portfolio_items = Portfolio.where(subtitile: ‘Ruby on Rails’)
+ @portfolio_items = Portfolio.where(subtitile: ‘Angular’)

Only one portfolio item will display, because only one uses that subtitle.

This is amazing adept as it gives us the ability to filter our items based on a database query. If you switch to the terminal, you can see all the database queries.

large

Remember when we were in the console and we saw the SQL query? These are similar to that. The system is performing the exact same query we ran in the console. You have the control to render whatever items you choose.

Custom Scopes

While using @portfolio_items = Portfolio.where(subtitile: ‘Angular’) in your controller is fine, it's not considered the best way of doing this. A best practice would be to build a custom scope. We've already experienced a little bit of custom scopes, however it was unintentional because Rails built it for us automatically.

Open your blog.rb file. This might help you to recall when we were building our enum method for blog status. We were able to run database queries such as Blog.draft which brought back all the draft items and Blog.published that brought back all the blog posts that where published. We can do the same thing, except we will build it ourselves. Enum does the same thing for us for free, and that is valuable when you are building out a work flow, but there are times when you may want to segment it out by some other method. In our case, eventually, I may want to build out multiple pages where one page displays my ‘Angular’ portfolios and another page displays my ‘Ruby on Rails’ portfolios.

Creating a Custom Scope

First let’s change our index method in our portfolios controller so there is no method

- @portfolio_items = Portfolio.all
+ @portfolio_items = Portfolio.

Next open portfolio.rb. This is where we will create our custom scope. There are two ways to do it, and I'm going to show you both.

For the first option we need to define a class method, using self which means we are referencing the current version of portfolio, to be exact, I am calling a particular portfolio.

def self.angular
end

Inside this method, I can paste my query using where

def self.angular
  where(subtitle: 'Angular')
end

Save the file. Next, go to your controller file and use your new method.

class PortfoliosController < ApplicationController
  def index
    @portfolio_items = Portfolio.angular
  end

Save your file. Now, if you refresh the browser, the query still works the way we wanted. This is considered a much better practice to use when it comes to implementing database queries. Instead of having different database queries and where statements inside our controller, we can keep it all in our model file. Remember, this is just one way to write this. Personally I like to use this format as it looks more like pure Ruby.

The other option is to call a scope, and use the scope keyword. You follow that with a name, then a comma , followed by a lambda. A lambda is a way of encapsulating an entire process in Ruby. The syntax is to use the arrow -> . followed by a block in curly braces { }. We will use the same type of query as before utilizing where

scope :ruby_on_rails_portfolio_items, -> { where(subtitle: 'Ruby on Rails') }

Save the file. Now we can treat the name of the scope ruby_on_rails_portfolio_items just like we did our angular method. In our portfolios_controller.rb file we can change our index call like this:

def index
  @portfolio_items = Portfolio.ruby_on_rails_portfolio_items
end

Save the file and then refresh the browser. This call will only return the "Ruby on Rails" items.

Custom scopes are very helpful and definitely considered a best practice in the Rails community. When you create custom scopes it forces you to keep all the logic for the model in the model file. A database query, like the one we just created for angular, really does belong in the model. This is important because the controller should only manage data flow. It should basically function like a traffic cop, where it is passing items in and letting them go to where they need to go. You shouldn’t have a lot of logic in the controller, if it’s something that should be relegated to the model, the model file is where it should be.

I'm going to revert the code in the index action of our portfolio_controller to the previous version of, Portfolio.all, as this is how I want it to behave.

class PortfoliosController < ApplicationController
  def index
    @portfolio_items = Portfolio.all
  end

Creating a Custom Action

We can add any custom actions we want to the controller. It is fairly easy to do. Create a method called angular. Inside, use your angular custom scope to call the specific items you want. We will call the instance variable angular_portfolio_items

  def angular
    @angular_portfolio_items = Portfolio.angular
  end

Next we need to create a view file. Go to app/views/portfolios and create a new file called angular.html.erb. Inside the file we can put our angular code. The easiest way will be to copy the code from our app/views/portfolios/index file and paste it our new file, Then make some small changes.

- <h1>Portfolio Items</h1>
+ <h1>Angular Portfolio Items</h1>
- <% @portfolio_items.each do |portfolio_item| %>
+ <% @angular_portfolio_items.each do |portfolio_item| %>

Your angular.html.erb file should now look like this

<h1>Angular Portfolio Items</h1>

<%= link_to "Create New Item", new_portfolio_url %>

<% @angular_portfolio_items.each do |portfolio_item| %>
  <p><%= link_to portfolio_item.title, portfolio_show_path(portfolio_item) %></p>
  <p><%= portfolio_item.subtitle %></p>
  <p><%= portfolio_item.body %></p>
  <%= image_tag portfolio_item.thumb_image unless portfolio_item.thumb_image.nil? %>
  <%= link_to "Edit", edit_portfolio_path(portfolio_item) %>
  <%= link_to 'Delete Portfolio Item', portfolio_path(portfolio_item), method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>

The last item we need to take care of is updating our routes file. We will add a get request for ’angular-items' and map it to ’portfolios#angular'

  resources :portfolios, except: [:show]
  get 'angular-items', to: 'portfolios#angular'
  get 'portfolio/:id', to: 'portfolios#show', as: ‘portfolio_show'

This is not the long term solution, but I just want to reiterate how the data flow works.

Now we just have to restart the rails server, and everything should work. Navigate to localhost:3000/angular-items, and the page behaves just as we wanted. We now have a route called angular-items that contains the angular portfolio items.

large

We can do for the same for "Ruby on rails” items as well, but this isn't the best way to do it because we have a hardcoded item in there. If you know for a fact that that is all you want that is perfectly fine. I'll keep the new route in the routes file for demonstration purposes because I want you to see how easy it is to create routes, map them to files, map those to controller items and use the custom scope to call your items. Hopefully you can appreciate how easy that is, and now we are able to pass all of those items in for a custom output.

Just a note: Part of the reason I am leaving both types of custom scopes in the portfolio.rb file is for your reference. I want to leave those items so you can come back and refresh your memory on how to do certain tasks and features.

Before we end, let's upload files to our GitHub branch. First we do a git status. Next, git add . followed by git commit -m ‘Implemented custom scopes for portfolio items’. Last we will push it up with git push origin data-feature.

Go ahead and update Pivotal tracker by checking off the task ‘create custom scopes’.

In the next guide we are going to be discovering how to implement virtual attributes.

Resources