Words and Code

One writer’s journey from words to code.

Taskmanaging Your App, Part 1: Using Rake

Rake tasks: we’ve all used ‘em. From migrating our database to seeding it, we run commands using rake all of the time! But what actually happens when you run a rake task? And where is all this stuff defined, exactly? And how do you write a customized rake task of your very own?

These were the questions I was asking myself last week, when I had to write a rake task to stage some data for an application I was working on. I knew exactly what my rake task was supposed to accomplish, and I had a general idea of the code that had to live inside of it. I didn’t quite know how to write my rake task, however. And I definitely didn’t know what was going on inside of the Ruby interpreter when it would read my yet-to-exist task.

So, I set out to answer some of those questions, and learned a bit about how rake works in the process. It was an interesting rabbit hole to dive down, particularly since I had never before questioned what was happening when I ran a rake command in my command line. When you’re first starting out with code, it’s okay to accept some of the obfuscation that is inherently a part of the abstraction of larger applications. But you should never go too long without questioning why and how a certain thing works the way that it does. And that’s exactly what we’ll do with our beloved rake commands.

Form And Function

We use the rake command so often that it might be easy to forget that it’s actually part of a gem! Most everything we use – including that little gem called rails – relies on the rake gem as a dependency. And everything we could ever want to know about rake’s form and function can be found in a single place: the Rakefile, a top-level file that exists in any application that uses this gem to manage and run tasks of all kinds.

So what’s inside of our Rakefile after we generate a brand new Rails application? Something that looks like this:

1
2
3
4
5
6
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require File.expand_path('../config/application', __FILE__)

Rails.application.load_tasks

Pretty empty, right? Except that there’s a pretty important line in there at the end which invokes a load_tasks method on our application object, which is what allows rake to load tasks into our application itself. But how do we fill up this Rakefile with some awesome rake tasks? Well, if we read carefully, it seems like rake is telling us exactly what we need to do.

Let’s start by creating a data staging task that will load some initial data into our application so that we have something to work with while in development. We’ll create a stage_data.rake file nested inside of our lib/tasks directories. But that’s empty too! You know what that means, don’t you? Time to write some tasks.

Managing Our Task Management

Since we’re working within the context of a mid-size Rails application, the first thing we probably want to do is namespace our rake tasks. We can do that a namespace block:

1
2
namespace :data do
end

This is how we can break up all the tasks that are concerned with managing data. But for now, let’s just work on writing a single task, which will stage our development environment with some sample data to help us while we’re building out the front end later on.

In order to write our rake task, we really just need two things: a description and then a task block. We know what we want our task to do, so that’s what we’ll put in our description. What we write here will be mostly to help us later on, when we want to know what tasks are available to us, and what exactly they do.

We’ll also want to specify any dependencies that this task might have.

After the desc term, we’re describing what our task will do, and then setting the command that we’ll use to run the task itself. We’re also specifying any dependencies that this task might have. In our case, the only thing that this task depends upon is the application itself. So, we want to make sure that our entire Rails application is loaded before running this task:

1
2
3
4
5
namespace :data do
  desc "Stage environment with sample data."
  task stage: :environment do
  end
end

Interestingly, all that’s happening here is that a method called task is being invoked, and the key-value pair that we provided it (stage: :environment) is telling task exactly which other methods to execute before running the subsequent task we have defined inside of the block. I like how this tutorial explains what’s going on behind the scenes:

It starts with the task method which takes a hash parameter. The key will be the name of the task. The value stored under that key, here :environment, indicates which other Rake tasks should be run before this task is run. Think of them like dependencies. When your task depends on :environment, it will load your entire Rails application. If your task doesn’t actually need Rails, don’t depend on :environment and you can greatly increase startup time and decrease memory usage.

Another cool thing to keep in mind is the description. We know that our task description (after the desc) will show up again at some point. But when, exactly? Well, it comes into play when we run the rake -T command, which lists out all the tasks available to our specific application:

1
2
 rake -T
rake data:stage          # Stage environment with sample data.

Cool! Whatever we write in our description is exactly what will be output if and when this command is run in the future. Now, how do we get this task to create some sample data every time it’s run? Well, we’ll want to use ActiveRecord in order to actually persist some objects to our database. There’s a handy block method on ActiveRecord::Base that does exactly this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace :data do
  desc "Stage environment with sample data."
  task stage: :environment do
      ActiveRecord::Base.transaction do
          poetry = Genre.create!(name: 'Poetry')

          POETRY_SUBGENRES = ['lyric', 'drama', 'epic']

          POETRY_SUBGENRES.each do |genre|
              SubGenre.create!(sub_genre: genre, parent_genre: poetry)
          end
      end
  end
end

Nice! We’re creating a genre and some sub genres, and associating them together. We could also add in authors and books to make this task a bit more robust. And to get really fancy, we could import a csv of all this information if it lived somewhere else!

Extending Our Tasks Further

Now that we know how to structure our rake tasks, we can add more tasks into the very same file. In fact, we can take advantage of the namespacing that we set up early on right now! Let’s create a rake data:reset task that will work almost the same as the task we wrote above. The only difference being that this task will destroy any data that was staged (or any that might be hanging around, for some strange reason).

We can write that task inside of our same file, inside of our namespace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace :data do
  desc "Stage environment with sample data."
  task stage: :environment do
      # the task we just wrote lives here!
  end

  desc "Resets and clears all the sample data."
  task reset: :environment do
    ActiveRecord::Base.transaction do
      Genre.destroy_all
      SubGenre.destroy_all
    end
  end
end

All we’re doing is using the handy methods that ActiveRecord gives us for free inside of the blocks we’ve defined in our task. And now when we run rake -T, we’ll see this:

1
2
3
 rake -T
rake data:stage          # Stage environment with sample data.
rake data:reset          # Resets and clears all the sample data.

Look at us! We’re rake task writing masters! We could get even fancier and make one task depend upon the other. Remember how the block syntax with the task method works? We can use the form of that syntax to make our task function the way we want to:

1
2
3
4
5
6
namespace :data do
  desc "Resets and stages the environment with sample data."
  task stage: [:environment, :reset] do
      # the task we wrote lives here!
  end
end

Now, we’re specifying that not only should the task method load the environment, but it should also run the rake data:reset task before running the rake data:stage task! This could be super useful in development, when you don’t want to have your database filled with duplicate data.

Another fun thing we could do is pass in parameters to our rake task:

1
2
3
4
5
6
7
8
namespace :data do
  desc "Stages the environment with specific amount of sample book data."
  task create_books: [:books_to_create] :environment do
    number_of_books = args[:books_to_create]
    # Uses the books_to_create variable to
    # create the number of books we want.
  end
end

Now, when we call the rake data:create_books[100] method, we could have our rake task generate 100 books for us based on the input argument we provided it.

This is pretty fun, right? We could put a ton of tasks that do all sorts of application management for us, and it would be no problem. Except that it would be, and here’s why: we never want too much logic in one single part of our application. In other words, we want to separate out what different sections of application are concerned with. We probably could abstract out a lot of what these tasks into something else (another object?), which would help keep our rake tasks nice and lean.

It turns out that this is a pretty common situation to run into, and not just with rake tasks! There’s a really cool pattern that implements a special kind of object that performs a service for you so that your code can stay simple, without too much logic in it. These objects are called service objects, and until I saw them in some production-level code, I had never really thought about their purpose. Tune in again next week, when I’ll dive into service objects and how they help us serparate concerns in our code. Until then, here’s a rake-themed gif to tide you over:

tl;dr?

  • Rake tasks are just Ruby code written inside of a either a .rake file in the /app/tasks directory, or sometimes within a Rakefile itself.
  • A rake task needs a description and a task block. The description is what will be seen when you run rake -T, and the task block is where you’ll write what the task actually has to do, and specify any dependencies.
  • Need to see another example of how to make a rake task? Check out this awesome tutorial on rake, or watch this RailsCast.