← 上一章:開發工具與常用命令列指令 下一章:變數、常數、流程控制、迴圈 →
第一個應用程式(使用 Scaffold)
在第二章使用 rails new
指令建立了一個全新的 Rails 專案,讓我們接著用這個專案繼續往下做。
你的第一個 Rails 應用程式(Blog 系統)
新手上路,讓我們來做一個讓使用者可以發文的 Blog 系統吧!先想一下這個系統的使用者故事(User Story)大概會長什麼樣子:
- 可以新增使用者(User)
- 每個使用者(User)可以新增、修改或刪除文章(Post)
雖然 Ruby 的世界有非常多厲害的套件(Gem),例如像是會員系統,只要用 devise 就可以在幾分鐘甚至是幾十秒內就把會員註冊、登入、登出、忘記密碼等會員基本的功能完成。不過這裡先不用任何套件,僅使用 Rails 內建的功能來完成。
使用者功能
Step 1: 使用 Scaffold
我們先想一下使用者的資料大概會長什麼樣子:
欄位名稱 | 資料型態 | 說明 |
---|---|---|
name | 字串(string) | 使用者姓名 |
字串(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 的新增、修改、刪除功能一口氣都做出來。
還好,工程師有著懶惰的美德,所以上面這串很長的指令,可以濃縮成更簡單的指令:
generate
可以簡寫成g
- 如果資料欄位是
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),裡面有 name
、email
以及 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
,應該可以看到這個畫面:
試著輸入一些資料:
你會發現你根本沒寫到什麼程式碼,一個簡單的 Scaffold 指令,已經把整個使用者的新增、修改、刪除的功能都完成了:
相當神奇吧!
文章功能
完成了使用者功能,接著是文章(Post)功能,大致上也是依樣畫葫蘆,但還會加上一些這兩個功能之間的關連性。
先想一下文章的資料大概會長什麼樣子:
欄位名稱 | 資料型態 | 說明 |
---|---|---|
title | 字串(string) | 文章標題 |
content | 文字(text) | 內文 |
is_available | 布林(boolean) | 文章是否上線 |
user_id | 數字(integer) | 使用者編號 |
這裡有幾個需要說明的地方:
- 文字(text)跟字串(string)不同的地方,是在於
text
型態可以存放更多的內容(因為通常文章不會只有短短幾個字)。 - 加入
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
說明:
- 除了
string
型態之外,其它型態不能省略。 - 雖然
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
:
在 User 的欄位先填寫數字 1
,表示是 1 號使用者:
這個 User 欄位其實不該讓使用者自己填空,至少是要自動帶入或是使用下拉選單,不過暫時先這樣。然後就可以看到:
畫面上出現了看起來有點像亂碼的東西 #<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 %>
應該就可以正常顯示了:
其實這裡還有一些效能問題(N + 1 Query)要處理一下。
什麼是 N + 1 Query?
讓我們看一下這個畫面:
這裡有 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
方法來減少不必要的資料庫查詢。把原來 PostsController
的 index
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