← 上一章:開發工具與常用命令列指令 下一章:變數、常數、流程控制、迴圈 →

第一個應用程式(使用 Scaffold)

第二章使用 rails new 指令建立了一個全新的 Rails 專案,讓我們接著用這個專案繼續往下做。

你的第一個 Rails 應用程式(Blog 系統)

新手上路,讓我們來做一個讓使用者可以發文的 Blog 系統吧!先想一下這個系統的使用者故事(User Story)大概會長什麼樣子:

  • 可以新增使用者(User)
  • 每個使用者(User)可以新增、修改或刪除文章(Post)

雖然 Ruby 的世界有非常多厲害的套件(Gem),例如像是會員系統,只要用 devise 就可以在幾分鐘甚至是幾十秒內就把會員註冊、登入、登出、忘記密碼等會員基本的功能完成。不過這裡先不用任何套件,僅使用 Rails 內建的功能來完成。

使用者功能

Step 1: 使用 Scaffold

我們先想一下使用者的資料大概會長什麼樣子:

欄位名稱 資料型態 說明
name 字串(string) 使用者姓名
email 字串(string) 使用者 Email
tel 字串(string) 聯絡電話

接下來,使用 Rails 內建的 Scaffold 功能來幫我們產生需要的檔案。切換到終端機畫面輸入指令:

$ bin/rails generate scaffold User name:string email:string tel:string
Running via Spring preloader in process 17922
      invoke  active_record
      create    db/migrate/20170613034005_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
      ..[略]..
      create      app/views/users/_user.json.jbuilder
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/users.coffee
      invoke    scss
      create      app/assets/stylesheets/users.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss

打上面這個指令的時候,記得要先用 cd 指令切到 Rails 專案目錄裡,不然會出現不正確的訊息。這個 Scaffold 指令產生了一堆檔案,我們在後面的章節會再做更詳細的介紹,現在你只要先記得這個指令會幫你把 User 的新增、修改、刪除功能一口氣都做出來。

還好,工程師有著懶惰的美德,所以上面這串很長的指令,可以濃縮成更簡單的指令:

  1. generate 可以簡寫成 g
  2. 如果資料欄位是 string,可以省略不寫,但如果是其它型態則不能省略。

所以原來的指令:

$ bin/rails generate scaffold User name:string email:string tel:string

可以簡寫成:

$ bin/rails g scaffold User name email tel

Step 2 把 migration 的描述具現化

在上一步產生的一堆檔案裡,有一個比較特別的檔案,在專案的 db/migrate 目錄裡,檔名可能像這樣: 20170613034005_create_users.rb (前面的數字是這個檔案產生的時間,所以應該會跟各位的檔名不太一樣),裡面的內容如下:

class CreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
      t.string :tel

      t.timestamps
    end
  end
end

內容現在看不懂沒關係,但大概可以猜得出來它是要建立一個表格(table),裡面有 nameemail 以及 tel 三個欄位,分別都是字串(string)型態。在 Rails 專案,這樣的檔案稱之遷移檔(migration file),是個很重要的檔案,在後面的章節會有更詳細的介紹。

現在要做的,就是執行這個遷移檔的描述,在資料庫建立一個名為 users 的表格,好讓我們可以把使用者的資料放進去。

$ bin/rails db:migrate
== 20170613034005 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0012s
== 20170613034005 CreateUsers: migrated (0.0013s) =============================

這個指令就是做這件事,這樣建立了一個名為 users 的空的資料表囉!

要特別注意的是,在 Rails 5 之前,這個指令是 rake db:migrate,在 Rails 5 之後,雖然原來的 rake 指令也可用,但為了統一,所以許多原來的 rake 指令都搬到 rails 底下了。

Step 3 啟動 Rails Server

到這裡,其實使用者的新增、修改、刪除功能已經完成了!這時候只要啟動 Rails Server 就行了。

$ bin/rails server
=> Booting Puma
=> Rails 5.1.1 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.9.1 (ruby 2.4.1-p111), codename: Private Caller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

如果想要少打幾個字,bin/rails server 指令也可簡化成 rails s。接著打開瀏覽器,連上網址 http://localhost:3000/users,應該可以看到這個畫面:

image

試著輸入一些資料:

image

你會發現你根本沒寫到什麼程式碼,一個簡單的 Scaffold 指令,已經把整個使用者的新增、修改、刪除的功能都完成了:

image

相當神奇吧!

文章功能

完成了使用者功能,接著是文章(Post)功能,大致上也是依樣畫葫蘆,但還會加上一些這兩個功能之間的關連性。

先想一下文章的資料大概會長什麼樣子:

欄位名稱 資料型態 說明
title 字串(string) 文章標題
content 文字(text) 內文
is_available 布林(boolean) 文章是否上線
user_id 數字(integer) 使用者編號

這裡有幾個需要說明的地方:

  1. 文字(text)跟字串(string)不同的地方,是在於 text 型態可以存放更多的內容(因為通常文章不會只有短短幾個字)。
  2. 加入 user_id 欄位的目的,是為了可以讓該篇文章跟某位使用者(user)連結在一起。

Step 1 使用 Scaffold

根據上面這個表格,我們使用 Scaffold 來產生相對應的功能:

$ bin/rails g scaffold Post title content:text is_available:boolean user:references
Running via Spring preloader in process 18657
      invoke  active_record
      create    db/migrate/20170613040437_create_posts.rbb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml
      invoke  resource_route
      ..[略]..
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/posts.coffee
      invoke    scss
      create      app/assets/stylesheets/posts.scss
      invoke  scss
   identical    app/assets/stylesheets/scaffolds.scss

說明:

  1. 除了 string 型態之外,其它型態不能省略。
  2. 雖然 user_id 也可以用 user_id:integer,但使用 user:references 會幫你完成更多細節,這部份也一樣會在後面的章節介紹。

Step 2 別忘了把描述具現化

跟前面的 User 一樣,Scaffold 又再產生了一個新的遷移檔,所以別忘了再執行一下這個遷移檔:

$ bin/rails db:migrate
== 20170613040437 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0056s
== 20170613040437 CreatePosts: migrated (0.0057s) =============================

Step 3 檢視成果

如果你剛剛的 Rails Server 還沒關掉(通常在開發過程不會特別關掉),打開網址 http://localhost:3000/posts

image

在 User 的欄位先填寫數字 1,表示是 1 號使用者:

image

這個 User 欄位其實不該讓使用者自己填空,至少是要自動帶入或是使用下拉選單,不過暫時先這樣。然後就可以看到:

image

畫面上出現了看起來有點像亂碼的東西 #<User:0x007fea03d05788>,事實上它是一個 User 物件,我們可以修正一下程式碼,讓它顯示出使用者的姓名:

請打開專案的 app/views/posts/index.html.erb 檔案,把第 21 行的 post.user 改成 post.user.name,像這樣:

<% @posts.each do |post| %>
  <tr>
    <td><%= post.title %></td>
    <td><%= post.content %></td>
    <td><%= post.user.name %></td>
    <td><%= post.is_available %></td>
    <td><%= link_to 'Show', post %></td>
    <td><%= link_to 'Edit', edit_post_path(post) %></td>
    <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>

應該就可以正常顯示了:

image

其實這裡還有一些效能問題(N + 1 Query)要處理一下。

什麼是 N + 1 Query?

讓我們看一下這個畫面:

image

這裡有 5 筆資料,看起來沒什麼問題,但翻一下它的 log 會看到:

Started GET "/posts" for 127.0.0.1 at 2017-06-13 12:52:16 +0800
Processing by PostsController#index as HTML
  Rendering posts/index.html.erb within layouts/application
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Rendered posts/index.html.erb within layouts/application (21.9ms)
Completed 200 OK in 108ms (Views: 103.5ms | ActiveRecord: 1.1ms)

為了顯示這 5 筆 Post 所關連的 User 的姓名資料,額外多做了 5 次的資料庫查詢(最後一次因為已經查詢過所以是 Cache),這就是所謂的 N + 1 查詢(1 次 Post 查詢 + N 次的關連資料查詢),這對資料庫來說是很浪費的。在 Rails 裡可使用 includes 方法來減少不必要的資料庫查詢。把原來 PostsControllerindex action:

def index
  @posts = Post.all
end

改成:

def index
  @posts = Post.includes(:user)
end

再看一下原來的 log:

Started GET "/posts" for 127.0.0.1 at 2017-06-13 12:56:56 +0800
Processing by PostsController#index as HTML
  Rendering posts/index.html.erb within layouts/application
  Post Load (0.3ms)  SELECT "posts".* FROM "posts"
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 3, 2, 4)
  Rendered posts/index.html.erb within layouts/application (73.4ms)
Completed 200 OK in 120ms (Views: 105.4ms | ActiveRecord: 2.6ms)

使用 includes 之後,SQL 查詢變成 IN 的寫法,原來的 N + 1 次也會變成 1 + 1 = 2 次了。

Rails 常用快速鍵

Rails 專案裡常用到的指令都有簡寫,可以讓你少敲幾個字:

原本的指令 簡寫 用途
bin/rails generate bin/rails g 用來產生各種需要的檔案,例如 scaffold、controller、model 等等
bin/rails destroy bin/rails d 可刪除產生器所產生的檔案
bin/rails server bin/rails s 啟動 Rails 伺服器,讓你可以檢視目前專案的成果
bin/rails console bin/rails c 類似 Ruby 的 IRB 介面,但是有載入整個 Rails 專案的環境,可以在這裡直接操作資料
bin/rails dbconsole bin/rails db 直接進到資料庫裡,使用 SQL 語法對資料庫進行存取
bundle install bundle 安裝套件
rake test rake 執行測試

小結

Scaffold 好用歸好用,我當年第一次接觸 Rails 就是被 Scaffold 給騙進來的。但實際在工作的時候不見得常用,比較常見是使用各別的產生器(generator)各別建立 Controller 或 Model,畢竟 Scaffold 一口氣生出太多用不到的檔案,有種用牛刀殺小雞的感覺。

基本上 Rails 是不可能靠用聽的或用看的就學得會的,一定多要練習,建議有空可試著照 Rails Guide 的這篇 Getting Started 操練一遍,應該就對 Rails 更有概念了,大家加油!

← 上一章:開發工具與常用命令列指令 下一章:變數、常數、流程控制、迴圈 →

Comments