高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

Ruby 語法放大鏡之「常在終端機裡下 rake db:migrate 指令,這個 rake 是什麼,後面那個 db:migrate 又是怎麼回事?」

Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。

在開發 Ruby on Rails 專案的過程中,一定都看過或用過 rake db:migrate 這個指令,大部份的教學資料可能只會跟你說「只要照著打這個指令就行了」,沒有太多詳細的介紹。

其實這個 rake 指令,是一位已過世但我非常景仰的大師 Jim Weirich 所開發的。如果各位曾經聽說過 Make 這個工具,Rake 就像是 Ruby 版的 Make,你可以用 Ruby 語法來編寫 makefile。

動手練習

隨便建一個目錄來練習一下:

$ 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'
/Users/user/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `<main>'
(See full trace by running task with --trace)'

執行 rake 指令後,它會預期在這個目錄有 rakefile, Rakefile, rakefile.rb, Rakefile.rb 這四個檔案任何一個檔案,但因為什麼都沒有所以出現上面的錯誤訊息。即然沒有,就做一個給它:

$ touch Rakefile
$ rake
rake aborted!
Don't know how to build task 'default' (see --tasks)
/Users/user/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `eval'
/Users/user/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `<main>'
(See full trace by running task with --trace)'

咦? 有 Rakefile 但還是有錯誤訊息,那是因為我們什麼都還沒寫,所以先打開 Rakefile 來寫一個簡單的寄信任務:

1
2
3
4
5
6
7
desc "寄發會員通知信"
task :send_email do
  # ... 寄發信件功能實作
  puts "Email Sent!"
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 是怎麼做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
desc "寄發會員通知信"
task :send_email do
  # ... 寄發信件功能實作
  puts "Email Sent!"
end

namespace :db do
  desc "Migrate the database"
  task :migrate do
    puts "migrating database!"
  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,它的內容長這樣:

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_relative 'config/application'

Rails.application.load_tasks

咦? 怎麼都空空的? 讓我們直接挖 Rails 的原始檔出來看看(以 Rails 5.0.0 beta 4 版本為例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 檔案:railties/lib/rails/tasks.rb

require 'rake'

# Load Rails Rakefile extensions
%w(
  annotations
  dev
  framework
  initializers
  log
  middleware
  misc
  restart
  routes
  tmp
).tap { |arr|
  arr << 'statistics' if Rake.application.current_scope.empty?
}.each do |task|
  load "rails/tasks/#{task}.rake"
end

這段程式碼的意思就是一口氣載入一堆放在 railties/lib/rails/tasks/ 目錄的一些檔案(.rake 檔),這些都是 Rails 預設會載入的任務。而那個 rake db:migrate 任務內容就藏在(這裡)。

要在 Rails 專案加上自己的 rake 指令?

在剛剛看到根目錄的 Rakefile 的最後一行寫到:

1
Rails.application.load_tasks

再繼續練習挖一下原始碼,可以翻到這個檔案,這個檔案大概 700 行左右,我只列出相關的程式碼。

1
2
3
4
5
6
7
8
9
10
11
12
# 檔案:railties/lib/rails/engine.rb

def load_tasks(app=self)
  require "rake"
  run_tasks_blocks(app)
  self
end

def run_tasks_blocks(*)
  super
  paths["lib/tasks"].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 章節。

Rails 5 的變化

提醒一下,在 Rails 5 之後,原本那些 rake 指令,例如:

$ rake db:migrate
$ rake routes

也都搬一份到 rails 指令底下囉,像是這樣:

$ rails db:migrate
$ rails routes

下次看到這樣的指令,不要以為是打錯字囉 :)

Comments