MVC 與 Rails


Rails應用程式支援MVC模型,MVC指的是Model/View/Controller,也就是將Web應用程式的組成劃分為模型、畫面與控制器三個角色,最原始的MVC定義是指桌面應用程式上的架構,這邊不予探討。

Web應用程式借鏡桌面應用程式MVC架構,取其 Model/View/Controller的職責劃分,並修改流程為適用於HTTP請求/回應特性,基本上你也可以稱這個修改後的架構為MVC,或者是 Model 2,或併稱為MVC/Model 2。

在MVC中,將Web應用程式劃分為模型、畫面與控制器:

  • 控制器(Controller)職責
    • 接受請求
    • 驗證請求
    • 判斷要轉發請求給哪個模型
    • 判斷要轉發請求給哪個畫面
  • 模型(Model)職責
    • 保存應用程式狀態
    • 執行應用程式商務邏輯(Business logic)
  • 畫面(View)職責
    • (查詢模型狀態)
    • 執行呈現邏輯(Presentation logic)組織回應畫面

一個大致的流程示意如下所示:

MVC 示意圖

模型的角色較為多元,有些模型負責與資料庫互動,有些模型負責商務邏輯,因此視應用程式設計而定,視圖可能查詢模型狀態以取得組織畫面時必要之資料。

若是簡單的CRUD(Create、Read、Update、Delete)應用程式,使用Rails來實現以上架構,可用下圖來表示:

Rails 的 MVC 實現

底下直接用實例來說明這個架構,首先從設定資料庫開始,Rails建立新專案時預設使用SQLite資料庫,預設的設定檔是在專案目錄中config/database.yml:

  • database.yml
# SQLite version 3.x
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
development:
adapter: sqlite3
database: db/development.sqlite3
pool: 5
timeout: 5000

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
adapter: sqlite3
database: db/test.sqlite3
pool: 5
timeout: 5000

production:
adapter: sqlite3
database: db/production.sqlite3
pool: 5
timeout: 5000

Rails 應用程式執行提供有開發環境(development environment)、測試環境(test environment)與產品環境(production environment)三種不同的環境,因此在database.yml中可以看到development、test、production三個設定區 段,預設是使用開發環境(除了database.yml中不同的設定之外,三種環境的設定檔,分別是在專案目錄中config/environments 目錄中的development.rb、test.rb與production.rb中)。

以下先使用database.yml的預設內容來建立資料庫,請在 安裝 Rails 中的hello資料夾中,執行以下指令:

~/hello\$ rake db:create

Rake是開發Ruby程式時使用的建構工具(Rake是Ruby Make的意思),以上執行了db:create任務,結果會在專案中db資料夾中產生development.sqlite3(預設已存在)與 test.sqlite3兩個檔案。接著要使用rails指令來產生一個資料庫Model:

~/hello\$ rails g model message name:string content:text is_read:boolean
      invoke  active_record
      create    db/migrate/20111214072231_create_messages.rb
      create    app/models/message.rb
      invoke    test_unit
      create      test/unit/message_test.rb
      create      test/fixtures/messages.yml

g就是generate的縮寫,先注意到以上產生的*_create_messages.rb與message.rb。 *_create_messages.rb記錄了稍後執行db:migrate時要作的動作,其中*是時間標記,用以避免名稱衝突,就之前的指令主要是建 立新表格:
  • *_create_messages.rb
class CreateMessages < ActiveRecord::Migration
def change
create_table :messages do |t|
t.string :name
t.text :content
t.boolean :is_read

t.timestamps
end
end
end

message.rb中定義了對應資料庫表格的模型:
  • message.rb
class Message < ActiveRecord::Base
end

ActiveRecord是Rails中提供ORM(Object-Relational Mapping)的元件,預設的message.rb只繼承ActiveRecord:Base定義的基本功能。

為了執行*_create_messages.rb的內容以建立表格,需要執行db:migrate任務:
~/hello\$ rake db:migrate

這會在db/development.sqlite3中建立messages表格,在Rails中,表格名稱實例是小寫複數名稱,以底線區隔每個單字。如 果有興趣看看使用了哪些SQL,可以看看log/development.log中的內容,Rails會在schema_migrations表格記錄執 行過migrate中哪些.rb,再度執行db:migrate並不會重複執行已執行過的.rb。

接下來先在rails console中進行資料的新增:
~/hello\$ rails c
Loading development environment (Rails 3.1.3)
> message = Message.new
> message.name = "caterpillar"
> message.content = "Hello! caterpillar! This is your first message. Welcome to Rails!"
> message.save
> Message.new(:name => "Justin", :content => "Hi! Justin!").save
> message = Message.find(1)
> message.name
=> "caterpillar"
> message.content
=> "Hello! caterpillar! This is your first message. Welcome to Rails!"
> message = Message.find(2)
> message.name
=> "Justin"
> message.content
=> "Hi! Justin!"
> message.update_attributes(:content => "Justin! Welcome to Rails!", :is_read => true)
> message.content
=> "Justin! Welcome to Rails!"
> message.is_read
=> true
> exit

其中c就是console 縮寫,以上是簡單的CRU示範,D(Delete)的方法則是destroy

到這邊為止,建立了上圖中ActiveRecord的部份,也就是代表Model的Message,接著來建立控制器:
~/hello\$ rails g controller messages
      create  app/controllers/messages_controller.rb
      invoke  erb
      create    app/views/messages
      invoke  test_unit
      create    test/functional/messages_controller_test.rb
      invoke  helper
      create    app/helpers/messages_helper.rb
      invoke    test_unit
      create      test/unit/helpers/messages_helper_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/messages.js.coffee
      invoke    scss
      create      app/assets/stylesheets/messages.css.scss

在Rails中,控制器的命名慣例是複數,接著開啟app/controllers/messages_controller.rb,新增粗體部份:
  • messages_controller.rb
class MessagesController < ApplicationController
def show
@message = Message.find(params[:id])
end

end

MessagesController就是上圖中ActionController的角色,show方法是 MessagesController的一個動作(Action),一個應用程式可能會有多個Controller,每個Controller中組織類似職責的動作,params方法傳回的物件可用來取得請求參數,這邊要根據請求參數id取得並顯示資料庫中id為1的資料,取得的資料設定給@message,被設定為實例變數的資料,可以在畫面中取得,例如可以在app/views/messages中新增一個show.html.erb檔案,並如下撰寫:
  • show.html.erb
<ul>
<li><%= @message.name %>'s message</li>
<li><%= @message.content %></li>
</ul>

這相當於定義上圖中ActionView的角色,erb是Embedded Ruby的縮寫,也就是在頁面中內嵌Ruby程式碼的意思,*.html.erb是指使用erb產生HTML格式的結果(也就是說,你也可以產生XML、 JavaScript、CSV等結果),<%=與%>間的傳回值,會與最後的HTML結合在一起傳回給瀏覽器。

接下來要設定上圖中Routing的角色,這要在config/routes.rb中新增以下內容:
  • routes.rb
Hello::Application.routes.draw do
get "messages/show" => "messages#show" # 也可以只寫get "message/show"
end

如上設定之後,對於http://localhost:3000/messages/show/的GET請求,都會分配給messages_controller.rb中的show方法處理,show方 法處理完後,預設會轉發給show.html.erb進行處理,如果=>左右的名稱除了/與#不同外,其它都是對應的,也可以只寫為get "messages/show"。接下來使用rails s啟動伺服器,使用http://localhost:3000/messages/show/?id=1就可以取得並顯示第一筆訊息,取得的HTML如 下:

<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<link href="/assets/application.css?body=1" media="screen" rel="stylesheet" type="text/css" />
<link href="/assets/messages.css?body=1" media="screen" rel="stylesheet" type="text/css" />
<script src="/assets/jquery.js?body=1" type="text/javascript"></script>
<script src="/assets/jquery_ujs.js?body=1" type="text/javascript"></script>
<script src="/assets/messages.js?body=1" type="text/javascript"></script>
<script src="/assets/application.js?body=1" type="text/javascript"></script>

<meta content="authenticity_token" name="csrf-param" />
<meta content="d4Mp27BjC/wwDNn9+WpTX4b1jeiOQU4UEFdIDGGodig=" name="csrf-token" />
</head>
<body>

<ul>
<li>caterpillar's message</li>
<li>Hello! caterpillar! This is your first message. Welcome to Rails!</li>
</ul>

</body>
</html>

除了先前定義的show.html.erb內容之外,這個HTML的內容還來自app/views/layout/applications.html.erb這個佈局(Layout)檔案,其中yeild的部份,就是show.html.erb定義的內容會出現的部份:

<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<%= stylesheet_link_tag "application" %>
<%= javascript_include_tag "application" %>
<%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>