Scope in Rails: Anatomy of a Rails Project

There have been many challenges in my time at Flatiron School, but few have been as rewarding as the work I’ve done on my Rails project. While my Ruby CLI and Sinatra apps helped to establish a degree of confidence that I could code, Rails has given me the motivation to experiment and push the boundaries of my knowledge — and has also shown me that I have the ability to troubleshoot many of my own problems.

However, there was one requirement for the project that stumped me — scope methods. This is largely because the subject was barely touched on in the preceding Rails module and all the advice I received from instructors and fellow students amounted to “read the documentation” (which, to be fair, you should absolutely do — it’s all in there). Unfortunately, I sometimes struggle with the technical language of the Rails documentation and my questions weren’t so much about how to implement scope methods, but rather what they would offer to my app in the first place?

My hope is to answer that question, so that anyone else of a similar mindset doesn’t have to struggle quite as hard to grasp the concept.

At their core, scopes are customized queries made to your database, designed to retrieve a specific type of data or data in a specific order. They are defined within the model of the class you would like to call them on and are particularly useful when trying to display content from your database in a more organized fashion.

Let’s demonstrate with my own app: LoreScroll. LoreScroll is a tool designed to help writers and storytellers coordinate their plot, character development and world-building. One of my models, World, contains setting specific data, such as the scale of the setting and a summary of what makes it unique. I decided early on in my project that I wanted a hub where users could view each other’s Worlds and inspire one another.

Once I created the index page to accomplish this, I realized that there was no defined pattern in which the data was displayed. Users would struggle with identifying and finding interesting Worlds. A simple, but useful means to solve this problem would be to arrange the World data by the most recent post, ensuring there was always something new and unique to look at. This is where scope finally clicked with me.

Here’s the basic code:

class World < ApplicationRecord
scope :most_recent, -> {order('created_at DESC')}
end

So what exactly am I doing here and how does it help me to achieve my goal of presenting my data by most recent entry?

First, within my World model, I define that I am creating a scope method. This method takes two arguments:

  1. A name, in this case :most_recent, which I can use to call the scope on the class.

With my scope defined, I can call it within my controller…

class WorldsController < ApplicationController   def index
@worlds = World.most_recent
end
end

…and use @worlds in my world index view to display the newly arranged information from my database.

<% @worlds.each do |w| %>
<p><%= link_to w.name, world_path(w) %><br>
Drafted by <%= w.user.author_name %> on <%=
w.created_at.strftime("%B %e, 20%y") %>
</p>
<% end %>

(To make this structure clearer to the user, I chose to call .strftime on created_at, which using the argument I passed in, would present the date in a more digestible format.)

This is the end result, a clear list of World objects descending from most recent:

But this alone doesn’t demonstrate the true power of scope — because technically, I could have done the same thing with a one off query in the WorldsController. Something like this:

class WorldsController < ApplicationController
def index
@worlds = World.order('created_at DESC')
end
end

There’s a problem here though. Every time I want to call this query, I’d have to re-write it — which would require a ton of unnecessary repetition. That’s never a good way to approach code.

By using a scope method within the model itself, I can call it anywhere in the app without having to repeat myself. For example, on the User’s show page, I might choose to display all of the worlds that belong to that User. If I wanted to, I could add my :most_recent scope to that array, arranging them in the same way you’d find them on the Worlds index page.

<h3>Your Worlds</h3>
<% @user.worlds.most_recent.each do |w| %>
<li><%= link_to w.name, world_path(w) %></li><br>
<% end %>

This is a much more efficient way to work and it also has other benefits. Namely, scopes are chainable, allowing for increased complexity in your queries. For example, you might want to filter only Stories of a specific Genre, while also returning those results in alphabetical order. This added complexity allows for more specific querying that can be very helpful in creating a positive user experience.

We can see chained scopes in action back on LoreScroll. Let’s take a look at my Character model.

class Character < ApplicationRecord
scope :order_by_name, -> {order('name ASC')}
scope :order_by_role, -> {order(:role)}
scope :main_characters, -> {where(role: "Main
Character").limit(8).order('name ASC')}
end

The first two scopes listed here are similar to what we saw in my World model:

  • :order_by_name returns characters in ascending alphabetical order based on their name.

But we can see that :main_characters is a bit more complex. I created this scope method because I wanted to display characters from a story on that story’s show page, but realized that this would take up a lot of real estate and could balloon out of control if the author started adding NPCs, supporting or tertiary characters. It seemed like the best option would be to highlight main characters specifically, while featuring those with smaller roles on the broader characters index page for each story.

Chaining was essential to accomplishing this. I started by defining a scope method called :main_characters. After my lambda, I first selected characters where role was equivalent to “Main Character”, as defined in the role column of my database. From there, I chained the .limit method with an argument of 8, which would limit the query results to no more than eight main characters — which seemed like a realistic limitation, even for a story as wide-ranging as an epic fantasy or ensemble piece. Finally, I ordered those results alphabetically by name.

As with the :most_recent scope in my World model, I updated my CharactersController, as well as the necessary views, to display my content on the site.

You can see that the main characters appear, in alphabetical order, but without side characters like Chewbacca or Grand Moff Tarkin.

This displays the flexibility that chaining your queries within a scope allows. You can refine your results in a way that is much more specific than simple alphabetization or recency.

Understanding scope was a huge breakthrough for me, and helped me to not only understand its own power, but the power of Rails as a whole. Rails is filled to the brim with tools to prevent repetition and improve your workflow efficiency. Validations prevent bad data from making it into the database, Before Actions allow code, such as conditionals to check if a user is logged in, to run before controller actions, and Scopes aid with quick, custom queries. There is so much to discover under the hood with Rails and it is very rewarding to uncover it.

Check out my Rails Project here for more information: https://github.com/GoldenPavilion/LoreScroll/tree/master