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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 aRakefile
itself. - A rake task needs a description and a
task
block. The description is what will be seen when you runrake -T
, and thetask
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.