# 常在終端機裡下 rake db:migrate 指令，這個 rake 是什麼，後面那個 db:migrate 又是怎麼回事?

> 

Published: 2016-04-30
URL: http://cdn.kaochenlong.com/rake

---

在開發 Ruby on Rails 專案的過程中，一定都看過或用過 `rake db:migrate` 這個指令，大部份的教學資料可能只會跟你說「只要照著打這個指令就行了」，沒有太多詳細的介紹。

其實這個 `rake` 指令，是一位已過世但我非常景仰的大師 [Jim Weirich](https://en.wikipedia.org/wiki/Jim_Weirich) 所開發的。如果各位曾經聽說過 Make 這個工具，Rake 就像是 Ruby 版的 Make，你可以用 Ruby 語法來編寫 makefile。

&lt;!--more--&gt;

## 動手練習

隨便建一個目錄來練習一下：

    $ cd /tmp
    $ mkdir rake_demo
    $ cd rake_demo
    $ rake
    rake aborted!
    No Rakefile found (looking for: rakefile, Rakefile, rakefile.rb, Rakefile.rb)
    /Users/user/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `eval&#39;
    /Users/user/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `&lt;main&gt;&#39;
    (See full trace by running task with --trace)&#39;

執行 `rake` 指令後，它會預期在這個目錄有 `rakefile`, `Rakefile`, `rakefile.rb`, `Rakefile.rb` 這四個檔案任何一個檔案，但因為什麼都沒有所以出現上面的錯誤訊息。既然沒有，就做一個給它：

    $ touch Rakefile
    $ rake
    rake aborted!
    Don&#39;t know how to build task &#39;default&#39; (see --tasks)
    /Users/user/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `eval&#39;
    /Users/user/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `&lt;main&gt;&#39;
    (See full trace by running task with --trace)&#39;

咦? 有 `Rakefile` 但還是有錯誤訊息，那是因為我們什麼都還沒寫，所以先打開 `Rakefile` 來寫一個簡單的寄信任務：

```ruby
desc &quot;寄發會員通知信&quot;
task :send_email do
  # ... 寄發信件功能實作
  puts &quot;Email Sent!&quot;
end

task default: [:send_email]
```

`task` 方法可以定義任務的名字，後面接的 block 就是它實作的內容(實作功能就留給大家了，這裡僅先使用 puts 方法把結果印出來)。最上面的 `desc` 則僅是這個任務的描述，如果有描述的話，在 `rake -T` 指令列出任務清單的時候就看得到說明了：

    $ rake -T
    rake send_email  # 寄發會員通知信

因為我在上面定義了一個叫做 `send_email` 的任務，如果要執行它的話，就是這樣下指令：

    $ rake send_email
    Email Sent!

又，因為這邊我在最後一行把預設的任務設定成 `send_email`，所以即使我只輸入 `rake` 指令，它也會執行 `rake send_email` 任務。

另外，如果任務沒有在 `rake -T` 的清單上不表示任務不存在，它可能只是 desc 沒寫而已，但一樣是可以執行的喔。

## 那 rake db:migrate 中間的冒號?

了解 rake 的基本操作後，回來看看 Rails 裡常用的那個 `rake db:migrate` 是怎麼做的：

```ruby
desc &quot;寄發會員通知信&quot;
task :send_email do
  # ... 寄發信件功能實作
  puts &quot;Email Sent!&quot;
end

namespace :db do
  desc &quot;Migrate the database&quot;
  task :migrate do
    puts &quot;migrating database!&quot;
  end
end

task default: [:send_email]
```

使用 `namespace` 方法，然後把任務包進去，這樣一來，任務的名字就會長得像這樣：

    $ rake -T
    rake db:migrate  # Migrate the database
    rake send_email  # 寄發會員通知信

而且可以正常執行：

    $ rake db:migrate
    migrating database!

大概就是這樣囉! 關於更多 Rake 的使用方式，請參閱 Rake 的 Github 網站說明 https://github.com/ruby/rake

## Rails 裡也是這樣嗎?

隨便開一個 Rails 專案，在根目錄應該可以看到一個 Rakefile，它的內容長這樣：

```ruby
# 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_relative &#39;config/application&#39;

Rails.application.load_tasks
```

咦? 怎麼都空空的? 讓我們直接挖 Rails 的[原始檔](https://github.com/rails/rails/blob/v5.0.0.beta4/railties/lib/rails/tasks.rb)出來看看(以 Rails 5.0.0 beta 4 版本為例)：

```ruby
# 檔案：railties/lib/rails/tasks.rb

require &#39;rake&#39;

# Load Rails Rakefile extensions
%w(
  annotations
  dev
  framework
  initializers
  log
  middleware
  misc
  restart
  routes
  tmp
).tap { |arr|
  arr &lt;&lt; &#39;statistics&#39; if Rake.application.current_scope.empty?
}.each do |task|
  load &quot;rails/tasks/#{task}.rake&quot;
end
```

這段程式碼的意思就是一口氣載入一堆放在 `railties/lib/rails/tasks/` 目錄的一些檔案(.rake 檔)，這些都是 Rails 預設會載入的任務。而那個 `rake db:migrate` 任務內容就藏在([這裡](https://github.com/rails/rails/blob/v5.0.0.beta4/railties/lib/rails/tasks/engine.rake))。

## 要在 Rails 專案加上自己的 rake 指令?

在剛剛看到根目錄的 Rakefile 的最後一行寫到：

```ruby
Rails.application.load_tasks
```

再繼續練習挖一下原始碼，可以翻到[這個檔案](https://github.com/rails/rails/blob/v5.0.0.beta4/railties/lib/rails/engine.rb)，這個檔案大概 700 行左右，我只列出相關的程式碼。

```ruby
# 檔案：railties/lib/rails/engine.rb

def load_tasks(app=self)
  require &quot;rake&quot;
  run_tasks_blocks(app)
  self
end

def run_tasks_blocks(*)
  super
  paths[&quot;lib/tasks&quot;].existent.sort.each { |ext| load(ext) }
end
```

看得出來 Rails 除了載入內建的任務外，其中這個 `load_tasks` 方法會把在專案裡 `lib/tasks` 目錄裡的任務檔也都一併讀進來。

除了挖原始碼看得出來之外，其實在 Rails 專案根目錄的 Rakefile 一打開的那兩行說明：

    # 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.

如果你想要加自己的 task 的話，就是寫在專案的 `lib/tasks/` 目錄裡。寫法跟上面寫 Rakefile 沒什麼兩樣，更多細節可參閱 Rails Guide 上的 Command Line 的 [Custom Rake Tasks](http://guides.rubyonrails.org/command_line.html#custom-rake-tasks) 章節。

## Rails 5 的變化

提醒一下，在 Rails 5 之後，原本那些 rake 指令，例如：

    $ rake db:migrate
    $ rake routes

也都搬一份到 `rails` 指令底下囉，像是這樣：

    $ rails db:migrate
    $ rails routes

下次看到這樣的指令，不要以為是打錯字囉 :)


