Ruby on rails 实战圣经:打造 CRUD 应用程序

Much of the essence ofbuilding a program is in fact the debugging of the specification. - FredBrooks, The Mythical Man-Month

初入门像Rails这样的功能丰富的开发框架,难处就像鸡生蛋、蛋生鸡的问题:要了解运作的原理,你必须了解其中的组件,但是如果个别学习其中的组件,又将耗费许多的时间而见树不见林。因此,为了能够让各位读者能够尽快建构出一个基本的应用程序,有个大局观。我们将从一个CRUD程序开始。所谓的CRUD即为CreateReadUpdateDelete等四项基本数据库操作,本章将示范如何做出这个基本的应用程序,以及几项Rails常用功能。细节的原理说明则待Part2后续章节。

RailsMVC组件

我们在第一章Ruby on Rails简介有介绍了什么是MVC架构,而在Rails中分成几个不同组件来对应:

  1. ActiveRecordRailsModel
  2. ActionPack包含了ActionDispatchActionControllerActionView,分别是RailsRoutingControllerView

MVC diagram

这张图标中的执行步骤是:

1.       浏览器发出HTTP request请求给Rails

2.       路由(Routing)根据规则决定派往哪一个ControllerAction

3.       负责处理的Controller Action操作Model

4.       Model存取数据库或数据处

5.       Controller Action将得到的数据喂给View

6.       回传最后的HTML成品给浏览

其中,路由主要是根据HTTP Method方法(GETPOST或是PUTDELETE)以及网址来决定派往到哪一个ControllerAction。例如,我们在「Rails起步走」一章中的get "welcome/say_hello" => "welcome#say"意思就是,将GETwelcome/say_hello的这个HTTP request请求,派往到welcomecontrollersay action

认识ActiveRecord操作数据库

ActiveRecordRailsORM(Object-relationalmapping)组件,让你可以使用面向对象语法来操作关系数据库,它的对应概念如下:

  1. 将数据库表格(table)对应到一个类别(class)
  2. 类别方法就是操作表格(table)
  3. 将数据库一列(row)对应到一个对象(object)
  4. 对象方法就是操作个别的数据(row)
  5. 将数据库字段(column)对应到对象的属性(object attribute)

第三章「Rails起步走」我们提到了Scaffold鹰架功能,有经验的Rails程序设计师虽然不用鹰架产生程序代码,不过还是会使用Railsgenerator功能来分别产生ModelController档案。这里让我们来产生一个Model

$ rails g model event name:string description:text is_public:boolean capacity:integer

这些指令必须要在Rails项目目录下执行,承第三章也就是demo目录下

接着执行以下指令就会建立数据表(如果是使用SQLite3数据库话,会产生db/development.sqlite3这个档案)

$ bundle exec rake db:migrate

为了观察ActiveRecord实际执行的SQL指令,让我们另开一个指令窗口,在项目目录下执行tail -f log/development.log

tail screenshot

接着,让我们使用rails console(可以简写为rails c) 进入控制台模式做练习:

# 新增
> event = Event.new
> event.name = "Ruby course"
> event.description = "fooobarrr"
> event.capacity = 20
> event.save # 儲存進資料庫,讀者可以觀察另一個指令視窗
> event.id # 輸出主鍵 1,在 Rails 中的主鍵皆為自動遞增的整數 ID
 
> event = Event.new( :name => "another ruby course", :capacity => 30)
> event.save
> event.id # 輸出主鍵 2,這是第二筆資料
 
# 查詢
> event = Event.where( :capacity => 20 ).first
> events = Event.where( ["capacity >= ?", 20 ] ).limit(3).order("id desc")
 
# 更新
> e = Event.find(1) # 找到主鍵為 1 的資料
> e.name # 輸出 Ruby course
> e.update_attributes( :name => 'abc', :is_public => false )
 
# 刪除
> e.destroy

irb一样,要离开rails console请输入exit。如果输入的程序乱掉没作用时,直接Ctrl+Z离开也没关系。

认识Migration建立数据表

Rails使用了Migration数据库迁移机制来定义数据库结构(Schema),档案位于db/migrate/目录下。它的目的在于:

  1. 让数据库的修改也可以纳入版本控制系统,所有的变更都透过撰写Migration档案执
  2. 方便应用程序更新升级,例如让软件从第三版更新到第五版,数据库更新只需要执行rake db:migrate
  3. 跨数据库通用,不需修改程序就可以用在SQLite3MySQLPostgres等不同数据

在上一节产生Model程序时,Rails就会自动帮你产生对应的Migration档案,也就是如db/migrate/20110519123430_create_events.rb的档案。Rails会用时间戳章来命名档案,所以每次产生档名都不同,这样可以避免多人开发时的冲突。其内容如下:

# db/migrate/20110519123430_create_events.rb
class CreateEvents < ActiveRecord::Migration
  def change
    create_table :events do |t|
      t.string :name
      t.text :description
      t.boolean :is_public
      t.integer :capacity
 
      t.timestamps
    end
  end
end

其中的create_table区块就是定义数据表结构的程序。上一节中我们已经执行过bundle exec rake db:migrate来建立此数据表。

Migration档案不需要和Model一一对应,像我们来新增一个Migration档案来新增一个数据库字段,请执行:

$ rails g migration add_status_to_events

如此就会产生一个空的 migration 档案在 db/migrate 目录下。Migration有提供 API 让我们可以变更数据库结构。例如,我们可以新增一个字段。输入rails g migration add_status_to_events然后编辑这个Migration档案:

# db/migrate/20110519123819_add_status_to_events.rb
class AddStatusToEvents < ActiveRecord::Migration
  def change
    add_column :events, :status, :string
  end
end

接着执行bundle exec rake db:migrate就会在events表格中新增一个status的字段,域类型是stringRails会记录你已经对数据库操作过哪些Migrations,像此例中就只会跑这个Migration而已,就算你多执行几次bundle exec rake db:migrate也只会对数据库操作一次。

Rails透过数据库中的schema_migrations这张table来记录已经跑过哪些Migrations

认识ActiveRecord数据验证(Validation)

ActiveRecord的数据验证(Validation)功能,可以帮助我们检查数据的正确性。如果验证失败,就会无法存进数据库。

编辑app/models/event.rb加入

class Event < ActiveRecord::Base
    validates_presence_of :name
end

其中的validates_presence_of宣告了name这个属性是必填。我们按Ctrl+Z离开控制台重新进入,或是输入reload!,这样才会重载。

> e = Event.new
> e.save # 回傳 false
> e.errors.full_messages
> e.name = 'ihower'
> e.save
> e.errors.full_messages # 顯示驗證失敗的原

呼叫save时,ActiveRecord就会验证数据的正确性。而这里因为没有填入name,所以回传false表示储存失败。

实做基本的CRUD应用程序

典型路由

我们在「Rails起步走」一章分别为welcome/say_hellowelcome设定路由,如果每个路径都需要一条条设定会太麻烦了。这一章我们先使用Rails的典型路由,编辑config/routes.rb将最下方的此行批注打开:

match ':controller(/:action(/:id(.:format)))'

典型路由很容易理解,它会将/foo/bar这样的网址自动对应到Controllerfoobar Action。我们再下一章中我们会再改用另一种RESTful路由方式。

列出所有数据

执行rails g controller events,编辑app/controllers/events_controller.rb加入

def index
  @events = Event.all
end

Event.all会抓出所有的数据,回传一个数组给实例变量(instancevariables)指派给@events。在Rails会让Action里的实例变量(也就是有@开头的变量)通通传到View样板里面可以使用。这个Action默认使用的样板是app/views/events/目录下与Action同名的档案,也就是接下来要编辑的app/views/events/index.html.erb,内容如下:

<% @events.each do |event| %>
  
  •   <%= event.name %>
      <%= link_to 'Show', :controller => 'events', :action => 'show', :id => event %>
      <%= link_to 'Edit', :controller => 'events', :action => 'edit', :id => event %>
      <%= link_to 'Delete', :controller => 'events', :action => 'destroy', :id => event %>
      
    <% end %>
    <%= link_to 'New event', :controller => 'events', :action => 'new' %>

    这个View迭代了@events数组并显示内容跟超链接,有几件值得注意的事情:

    <%<%=不太一样,前者只执行不输出,像用来迭代的eachend这两行就不需要输出。而后者<%=里的结果会输出给浏览器。

    link_to建立超链接到一个特定的位置,这里为浏览、编辑和删除都提供了超链接。

    Rails3之前的版本,你必须使用<%=h event.name %>如此HTML才会被逸出防止XSS网络攻击。在Rails3之后默认就会逸出。如果不要逸出,请使用<%= raw event.name %><%=event.name.html_safe! %>网络安全一章有针对XSS进一步的说明

    连往http://localhost:3000/events就会看到这一页。目前还没有任何数据,让我们继续实现点击Newevent超链接之后的动作。

    新增数据

    建立一篇新的活动需要两个Actions。第一个是newAction,它用来实例化一个空的Event对象,编辑app/controllers/events_controller.rb加入

    def new
      @event = Event.new
    end

    这个app/views/events/new.html.erb会显示空的Event给用户:

    <%= form_for @event, :url => { :controller => 'events', :action => 'create' } do |f| %>
        <%= f.label :name, "Name" %>
        <%= f.text_field :name %>
     
        <%= f.label :description, "Description" %>
        <%= f.text_area :description %>
     
        <%= f.submit "Create" %>
    <% end %>

    这个form_for的程序代码区块(Codeblock)被用来建立HTML窗体。在区块中,你可以使用各种函式来建构窗体。例如f.text_field :name建立出一个文字输入框,并填入@eventname属性数据。但这个窗体只能基于这个Model有的属性(在这个例子是namedescription)Rails偏好使用form_for而不是让你手写窗体HTML,这是因为程序代码可以更加简洁,而且可以明确地链接在Model对象上。

    form_for区块也很聪明,Newevent的窗体跟Edit event的窗体,其中的送出网址跟按钮文字会不同的(根据@event的不同,前者是新建的,后者是已经建立过的)

    如果你需要建立任意字段的HTML窗体,而不绑在某一个Model上,你可以使用form_tag函式。它也提供了建构窗体的函式而不需要绑在Model实例上。我们会在ActionView: Helpers一章介绍

    当一个用户点击窗体的Create按钮时,浏览器就会送出数据到ControllercreateAction。也是一样编辑app/controllers/events_controller.rb加入:

    def create
      @event = Event.new(params[:event])
      @event.save
     
      redirect_to :action => :index
    end

    create Action会透过从窗体传进来的数据,也就是Rails提供的params参数(这是一个Hash),来实例化一个新的@event对象。成功储存之后,便将用户重导(redirect)indexAction显示活动列表。

    显示个别数据

    当你在index页面点击show的活动链接,就会前往http://localhost:3000/events/show/1这个网址。Rails会呼叫showaction并设定params[:id]1。以下是show Action

    编辑app/controllers/events_controller.rb加入

    def show
      @event = Event.find(params[:id])
    end

    这个show Actionfind方法从数据库中找出该篇活动。找到数据之后,Railsshow.html.erb样板显示出来。新增app/views/events/show.html.erb,内容如下:

    <%= @event.name %>
    <%= simple_format(@event.description) %>
     

    <%= link_to 'Back to index', :controller => 'events', :action => 'index' %>

    其中simple_format是一个内建的ViewHelper,它的作用是可以将换行字符\n置换成
    ,有基本的HTML换行效果。

    编辑数据

    如同建立新活动,编辑活动也有两个步骤。第一个是请求特定一篇活动的edit页面。这会呼叫ControllereditAction,编辑app/controllers/events_controller.rb加入

    def edit
      @event = Event.find(params[:id])
    end

    找到要编辑的活动之后,Rails接着显示edit.html.erb页面,新增app/views/events/edit.html.erb档案,内容如下:

    <%= form_for @event, :url => { :controller => 'events', :action => 'update', :id => @event } do |f| %>
        <%= f.label :name, "Name" %>
        <%= f.text_field :name %>
     
        <%= f.label :description, "Description" %>
        <%= f.text_area :description %>
     
        <%= f.submit "Update" %>
    <% end %>

    这里跟new Action很像,只是送出窗体后,是前往ControllerupdateAction

    def update
      @event = Event.find(params[:id])
      @event.update_attributes(params[:event])
     
      redirect_to :action => :show, :id => @event
    end

    update Action里,Rails一样透过params[:id]参数找到要编辑的数据。接着update_attributes方法会根据窗体传进来的参数修改到数据上。如果一切正常,用户会被导向到活动的show页面。

    删除数据

    最后,点击Destroy超链接会前往destroy Action,编辑app/controllers/events_controller.rb加入

    def destroy
      @event = Event.find(params[:id])
      @event.destroy
     
      redirect_to :action => :index
    end

    destroy方法会删除对应的数据库数据。完成之后,将用户导向index页面。

    Rails的程序风格非常注重变量命名的单数复数,像上述的index Action中是用@events复数命名,代表这是一个群集数组。其他则是用@event单数命名

    认识版型(Layout)

    Layout可以用来包裹样板,让不同样板共享相同的HTML开头和结尾部分。当Rails要显示一个样板给浏览器时,它会将样板的HTML放到LayoutHTML之中。默认的Layout档案是app/views/layouts/application.html.erb,其中yield就是会被替换成样板的地方。所有的样版默认都会套这个Layout。我们会再ActionView一章中介绍如何更换不同Layout

    现在,让我们修改Layout中的</span></span></code><span style="color: black; font-family: 宋体; mso-bidi-font-family: 宋体;">:</span><span style="color: black;"> </span></p> <p></p> <pre><code><span style="color: black; font-size: 12pt; mso-fareast-font-family: 宋体; mso-fareast-theme-font: major-fareast;"><span style="font-family:Lucida Console;background-color: rgb(246, 246, 246);"><!DOCTYPE html></span></span></code></pre> <pre><code><span style="color: black; font-size: 12pt; mso-fareast-font-family: 宋体; mso-fareast-theme-font: major-fareast;"><span style="font-family:Lucida Console;background-color: rgb(246, 246, 246);"><html></span></span></code></pre> <pre><code><span style="color: black; font-size: 12pt; mso-fareast-font-family: 宋体; mso-fareast-theme-font: major-fareast;"><span style="font-family:Lucida Console;background-color: rgb(246, 246, 246);"><head></span></span></code></pre> <pre><code><span style="color: black; font-size: 12pt; mso-fareast-font-family: 宋体; mso-fareast-theme-font: major-fareast;"><span style="font-family:Lucida Console;"><span style="mso-spacerun: yes;"><span style="background-color: rgb(246, 246, 246);">  </span></span><span style="background-color: rgb(246, 246, 246);"><title><%= @page_title || "Event application" %>

      <%= stylesheet_link_tag    "application" %>
      <%= javascript_include_tag "application" %>
      <%= csrf_meta_tags %>
     
    <%= yield %>
     

    如此我们可以在show Action中设定@page_title的值:

    def show
      @event = Event.find(params[:id])
      @page_title = @event.name
    end

    这样的话,进去show页面的title就会是活动名称。其他页面因为没有设定@page_title,就会是”Eventapplication”

    认识局部样板(PartialTemplate)

    利用局部样板(Partial)机制,我们可以将重复的样板独立出一个单独的档案,来让其他样板共享引用。例如new.html.erbedit.html.erb都有以下相同的样板程序:

    <%= f.label :name, "Name" %>
    <%= f.text_field :name %>
     
    <%= f.label :description, "Description" %>
    <%= f.text_area :description %>

    一般来说,新增和编辑时的表单域都是相同的,所以让我们将这段样板程序独立出一个局部样板,这样要修改字段的时候,只要修改一个档案即可。局部样板的命名都是底线_开头,新增一个档案叫做_form.html.erb,内容就如上。如此new.html.erb就可以变成:

    <%= form_for @event, :url => { :controller => 'events', :action => 'create' } do |f| %>
        <%= render :partial => 'form', :locals => { :f => f } %>
        <%= f.submit "Create" %>
    <% end %>

    edit.html.erb则是:

    <%= form_for @event, :url => { :controller => 'events', :action => 'update', :id => @event } do |f| %>
        <%= render :partial => 'form', :locals => { :f => f } %>
        <%= f.submit "Update" %>
    <% end %>

    透过<%= render :partial =>'form', :locals => { :f => f } %>会引用_form.html.erb这个局部样板,并将变量f传递进去变成局部变量。

    before_filter方法

    透过before_filter,我们可以将Controller中重复的程序独立出来。

    events_controller.rb上方新增

    before_filter :find_event, :only => [ :show, :edit, :update, :destroy]

    在最下方新增函式如下:

    protected
     
    def find_event
      @event = Event.find(params[:id])
    end

    Controller中的公开(public)方法都是Action,也就是可以让浏览器呼叫使用的动作。使用protectedprivate可以避免内部方法被当做Action使用

    删除showeditupdatedestroy方法中的

    @event = Event.find(params[:id])

    加入数据验证

    我们在数据验证一节中,已经加入了name的必填验证,因此当用户送出没有name的窗体,就会无法储存进数据库。我们希望目前的程序能够在验证失败后,提示用户储存失败,并让用户有机会可以修改再送出。

    修改app/controllers/events_controller.rbcreateupdateAction

    def create
      @event = Event.new(params[:event])
      if @event.save
        redirect_to :action => :index
      else
        render :action => :new
      end
    end

    如果活动因为验证错误而储存失败,这里会回传给用户带有错误讯息的new Action,好让用户可以修正问题再试一次。实际上,render :action => "new"会回传newAction所使用的样板,而不是执行new action这个方法。如果改成使用redirect_to会让浏览器重新导向到new Action,但是如此一来@event就被重新建立而失去用户刚输入的数据。

    def update
      if @event.update_attributes(params[:event])
        redirect_to :action => :show, :id => @event
      else
        render :action => :edit
      end
    end

    更新时也是一样,如果验证有任何问题,它会显示edit页面好让用户可以修正数据。

    而为了可以在储存失败时显示错误讯息,接着编辑_form.html.erb中加入

    <% if @event.errors.any? %>
          
          <% @event.errors.full_messages.each do |msg| %>
            
  • <%= msg %>
  •       <% end %>
          
    <% end %>

    认识Flash讯息

    请在app/views/layouts/application.html.erb Layout档案之中,yield之前加入:

    <%= flash[:notice] %>

    <%= flash[:alert] %>

    接着让我们回到app/controllers/events_controller.rb,在createAction中加入

    flash[:notice] = "event was successfully created"

    update Action中加入

    flash[:notice] = "event was successfully updated"

    destroy Action中加入

    flash[:alert] = "event was successfully deleted"

    event was successfullycreated」讯息会被储存在Rails的特殊flash变量中,好让讯息可以被带到另一个action,它提供用户一些有用的信息。在这个createAction中,用户并没有真的看到任何页面,因为它马上就被导向到新的活动页面。而这个flash变量就带着讯息到下一个Action,好让用户可以在showAction页面看到 event was successfully created.」这个讯息。

    分页外挂

    上述的程序用Event.all一次抓出所有活动,这在数据量一大的时候非常浪费性能和内存。通常会用分页机制来限制抓取数据的笔数。

    编辑Gemfile加入以下程序,这个档案设定了此应用程序使用哪些套件。这里我们使用kaminari这个分页套件:

    gem "kaminari"

    执行bundle install就会安装。装好后需要重新启动服务器才会加载。

    修改app/controllers/events_controller.rbindexAction如下

    def index
      @events = Event.page(params[:page]).per(5)
    end

    编辑app/views/events/index.html.erb,加入

    <%= paginate @events %>

    连往http://localhost:3000/events/,你可能需要多加几笔数据就会看到分页链接了。


     

    RESTful 应用程序

    什么是RESTful

    The first 90% of the codeaccounts for the first 90% of the development time. The remaining 10% of thecode accounts for the other 90% of the development time. – Tom Cargill, 贝尔实验室的面向对象程序专

    RESTful路由设计是Rails的独到创新,它使用了REST概念来建立一整组的命名路由(namedroutes)

    什么是REST? 表象化状态转变RepresentationalState Transfer,简称REST,是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。相较于SOAPXML-RPC更为简洁容易使用,也是众多网络服务中最为普遍的API格式,像是AmazonYahoo!Google等提供的API服务均有REST接口。

    REST有主要有两个核心精神:1. 使用Resource来当做识别的资源,也就是使用一个URL网址来代表一个Resource 2. 同一个Resource则可以有不同的Representations格式变化。这一章的路由实现了Resource概念,而Representation则是用respond_to方法来实现,我们会在Controller一章介绍到。

    关于REST的理论可以参考笔者整理的什么是RESTRESTful?。不过,了解理论并不是在Rails中使用RESTful路由的前提条件,所以大可以跳过不甚理解没关系。我们只要知道它可以带来什么技术上的具体好处,以及如何使用就足够了

    RESTful带给Rails最大的好处是:它帮助我们用一种比较标准化的方式来命名跟组织ControllersActions。在没有RESTful之前,我们上一章介绍了典型路由设计方式,也就是一个个指定ControllerAction,虽然十分地简便,但是却没有什么准则。同一个Action让不同的开发者设计,就很可能放在不同的Controller之下,更常见的是让一个Controller放太多不相关的Action,造成单一Controller过于庞大。

    RESTful带入Rails路由系统的点子,出自它对应了HTTP动词POSTGETPUTDELETE到数据的新增、读取、更新、删除等四项操作。一旦将HTTP动词考虑进来,如此我们就将原本的路由

    1. /events/create
    2. /events/show/1
    3. /events/update/1
    4. /events/destroy/1

    变成

    1. POST /events对应到Controller中的create action
    2. GET /events/1对应到Controller中的show action
    3. PUT /events/1对应到Controller中的update action
    4. DELETE /events/1对应到Controller中的destroy action

      什么是HTTPmethod?在HTTP 1.1通讯协议中制定了九种动词(Verbs)来跟服务器沟通,分别是HEADGETPOSTPUTDELETETRACEOPTIONSCONNECTPATCH。其中最常见的就是GETPOSTGET用来读取数据,这个动作不应该造成任何数据变更。而POST用于送出数据,这个动作不会被快取。而因为HTML只能送出GET或透过窗体送出POSTRails为了突破这个限制,在POST加上一个隐藏参数_method=PUT_method=DELETE就可以当做PUTDELETE请求了

      HTTP GET和其他动词最大的差别在于它被认为是一个纯读取、不会修改任何数据的操作,不像POSTPUTDELETE会修改服务器上的数据。我们一般用浏览器GET网页,可以回上一页或重新整理,但是POST网页要重新整理时,浏览器会提示你是否要在执行一次,就是这个道理

    Rails用这套惯例来大大简化了路由设定。那程序该怎么写呢? 我们在config/routes.rb加入以下一行程序:

    resources :events

    如此就会自动建立四个命名路由(named routes),搭配四个HTTP动词,对应到七个Actions。它的实际作用,就如同以下的设定:

    get    '/events'          => "events#index",   :as => "events"
    post   '/events'          => "events#create",  :as => "events"
    get    '/events/:id'      => "events#show",    :as => "event"
    put    '/events/:id'      => "events#update",  :as => "event"
    delete '/events/:id'      => "events#destroy", :as => "event"
    get    '/events/new'      => "events#new",     :as => "new_event"
    get    '/events/:id/edit' => "events#edit",    :as => "edit_event"

    用这张表格会更清楚:

    Helper

    GET

    POST

    PUT

    DELETE

    event_path(@event)

    /events/1
    show action

     

    /events/1
    update action

    /events/1
    destroy action

    events_path

    /events
    index action

    /events
    create action

         

    edit_event_path(@event)

    /events/1/edit
    edit action

           

    new_event_path

    /events/new
    new action

           

    注意到这七个Action方法的名字,Rails是定好的,无法修改。这一套惯例建议你背起来,你可以这样记忆:

    1. showneweditupdatedestroy是单数,对单一元素操
    2. indexcreate是复数,对群集操
    3. event_path(@event)需要参数,根据HTTP动词决定showupdatedestroy
    4. events_path毋需参数,根据HTTP动词决定indexcreate

    因此,最后我们不写:

    link_to event.name, :controller => 'events', :action => :show , :id => event.id

    而改写成:

    link_to event.name, event_path(event)

    而且只需记得resources就可以写出URLHelper

    [custom route]_event[s]_path( event ), :method => GET | POST | PUT | DELETE

    _path结尾是相对网址,而_url结尾则会加上完整Domain网址。

    浏览器支持PUTDELETE吗?Rails其实偷藏了_method参数。HTML规格只定义了GET/POST,所以HTML窗体是没有PUT/DELETE的。但是XmlHttpRequest规格(也就是Ajax用的)有定义GET/POST/PUT/DELETE/HEAD/OPTIONS

    修改成一个RESTful版本的CRUD

    根据第一节所学到RESTful技巧,接续上一章的CRUD应用程序,来改造成RESTful应用程序,相信各位读者可以从中发现到RESTful所带来的简洁好处。让我们开始动手修改吧:

    Step. 1

    编辑config/routes.rb,加入一个Resources

    resources :events

    请加在上方,routes.rb里面越上面的规则优先权较高。

    Step. 2

    编辑app/views/events/index.html.erb,修改各个link_to的路径:

    <% @events.each do |event| %>
      
  •     <%= link_to event.name, event_path(event) %>
        <%= link_to 'edit', edit_event_path(event) %>
        <%= button_to 'delete', event_path(event), :method => :delete %>
      
    <% end %>
     
    <%= link_to 'new event', new_event_path %>

    注意到删除的地方,我们多一个参数:method => :delete。非GET的操作,顾及网页亲和力可以把link_to改成用button_tolink_to如果浏览器的JavaScript没开,就会无法送出GET之外的操作。button_to就无此困扰,因为Rails是产生form卷标夹带_method参数。

    Step. 3

    编辑app/views/events/show.html.erb,修改link_to的路径:

    <%= @event.name %>
    <%= simple_format(@event.description) %>
     

    <%= link_to 'back to index', events_path %>

    Step. 4

    修改app/views/events/new.html.erb的窗体送出位置如下:

    <%= form_for @event, :url => events_path do |f| %>

    在本例中,你也可以完全省略:url参数,Rails可以根据@event推算出路由

    Step. 5

    修改app/views/events/edit.html.erb的窗体送出位置如下:

    <%= form_for @event, :url => event_path(@event), :method => :put do |f| %>

    :url:method也可以省略,Rails会根据@event是新建的还是修改来推算出要不要使用PUT

    Step. 6

    修改app/controllers/events_controller.rb,将createActiondestroy Action里的redirect_to改成

    redirect_to events_url

    update Action中的redirect_to改成

    redirect_to event_url(@event)

    Step.7

    一旦完成RESTful之后,我们在上一章一开始设定的典型路由就用不到了,编辑config/routes.rb将以下程序批注掉:

    # This is a legacy wild controller route that's not recommended for RESTful applications.
    # Note: This route will make all actions in every controller accessible via GET requests.
    # match ':controller(/:action(/:id(.:format)))'

    前两行的批注告诉你,这种典型路由已经不被新的RESTful风格所推荐使用。特别是它会让所有Actions都可以透过GET读取到,例如接收窗体的createAction最好只允许POST请求,但是打开典型路由就会让GET请求也可以作用

    常见错误

    Unknown action

    明明有在config/routes.rb里面定义了resources路由,但是出现以下的Unknownaction错误:

    Unknown action

    排除打错字之外,其原因多半是跟routes.rb里面的定义顺序有关。注意到在routes.rb里面,越上面的路由规则越优先,例如如果你定义成:

    Demo::Application.routes.draw do
        match ':controller(/:action(/:id(.:format)))'
        resources :events
    end

    那么网址/events/4就会优先比对到:controller/:action而去找4这个Action,这就错了。

    Routing Error

    这错误通常发生在link_to里,它抱怨找不到适合的路由规则来产生网址:

    Routing Error

    如果你是用典型路由,那么如以下程序乱给一个不存在的Controller,就会产生一样的错误了:

    link_to "foobar", :controller => "No such controller", :action => "blah"

    因为{ :controller => "Nosuch controller", :action => "blah" }比对不出有这个路由规则。但是如果是用RESTful路由呢?那多半是因为参数传错了,例如:

    link_to "Show", event_path(@foobar)

    这个@foobar没有定义所以是nilevent_path(@foobar)Rails内部来说等同于{ :controller => "events", :action =>"show", :id => nil },这就造成了找不到路由的错误,它必须知道:id才能知道是那一个活动的show Action网址。

    使用respond_to

    respond_to可以让我们在同一个Action中,支持不同的数据格式,例如XMLJSONAtom等。让我们来实现看看。

    Atom是一种基于XML的供稿格式,被设计为RSS的替代品,广泛应用于Blog feed

    Step. 1

    修改app/controllers/events_controller.rbindexAction加上XMLJSONAtom的支持,其中to_xmlto_jsonActiveRecord内建的方法:

    def index
      @events = Event.page(params[:page]).per(5)
     
      respond_to do |format|
        format.html # index.html.erb
        format.xml { render :xml => @events.to_xml }
        format.json { render :json => @events.to_json }
        format.atom { @feed_title = "My event list" } # index.atom.builder
      end
    end

    新增app/views/events/index.atom.builder档案,内容如下:

    atom_feed do |feed|
      feed.title( @feed_title )
      feed.updated( @events.last.created_at )
      @events.each do |event|
        feed.entry(event) do |entry|
          entry.title( event.name )
          entry.content( event.description, :type => 'html' )
        end
      end
    end

    打开浏览器分别浏览看看http://localhost:3000/events.xmlhttp://localhost:3000/events.jsonhttp://localhost:3000/events.atom这几个附档名不同的网址。

    Step. 2

    修改app/controllers/events_controller.rbshowAction加上XMLJSON的支持,这回我们试试看比较手工的方式,用Builder格式来建构XML,以及手动组Hash再转成JSON字符串:

    def show
      @event = Event.find(params[:id])
      respond_to do |format|
        format.html { @page_title = @event.name } # show.html.erb
        format.xml # show.xml.builder
        format.json { render :json => { id: @event.id, name: @event.name }.to_json }
      end
    end

    注意到{id: @event.id, name: @event.name }Ruby1.9才支持的语法,使用Ruby 1.8的朋友请改用{:id => @event.id, :name => @event.name }

    编辑app/views/events/show.xml.builder

    xml.event do |e|
      e.name @event.name
      e.description @event.description
    end

    打开浏览器分别浏览看看http://localhost:3000/events/1.xmlhttp://localhost:3000/events/1.json等网址。

    Step. 3

    如果想要加上这些格式的超链接,可以在URL Helper中传入:format参数。让我们修改app/views/events/index.html.erb加上不同格式的超链接:

    <% @events.each do |event| %>
      
  •     <%= link_to event.name, event_path(event) %>
        <%= link_to " (XML)", event_path(event, :format => :xml) %>
        <%= link_to " (JSON)", event_path(event, :format => :json) %>
        <%= link_to 'edit', edit_event_path(event) %>
        <%= button_to 'delete', event_path(event), :method => :delete %>
      
    <% end %>
     
    <%= link_to 'new event', new_event_path %>
    <%= link_to "Atom feed", events_path(:format => :atom) %>

    行数统计

    到目前为止,总共写了多少程序了呢?Rails提供了一个简单的指令可以知道:

    $ bundle exec rake stats

    就会输出这样的表格:

    +----------------------+-------+-------+---------+---------+-----+-------+
    | Name                 | Lines |   LOC | Classes | Methods | M/C | LOC/M |
    +----------------------+-------+-------+---------+---------+-----+-------+
    | Controllers          |    86 |    61 |       2 |       7 |   3 |     6 |
    | Helpers              |     4 |     4 |       0 |       0 |   0 |     0 |
    | Models               |     2 |     2 |       1 |       0 |   0 |     0 |
    | Libraries            |     0 |     0 |       0 |       0 |   0 |     0 |
    | Integration tests    |     0 |     0 |       0 |       0 |   0 |     0 |
    | Functional tests     |    49 |    39 |       1 |       0 |   0 |     0 |
    | Unit tests           |    11 |     6 |       2 |       0 |   0 |     0 |
    +----------------------+-------+-------+---------+---------+-----+-------+
    | Total                |   152 |   112 |       6 |       7 |   1 |    14 |
    +----------------------+-------+-------+---------+---------+-----+-------+
      Code LOC: 67     Test LOC: 45     Code to Test Ratio: 1:0.7

    其中LOC是指不包含空行的行数。

    如何除错?

    如果是Model中的程序,你可以在命令行下输入rails console,然后在Console中呼叫看看Model的方法看看正确与否。而除错ControllerViews一个简单的方法是你可以使用debug这个Helper方法,例如在app/views/events/show.html.erb中插入:

    <%= debug(@event) %>

    这样就会输出@event这个值的详细内容。不过,更为常见的是使用Logger来记录信息到log/development.log里。

    关于Logger

    Rails环境中,你可以直接使用logger或是Rails.logger来拿到这个Logger对象,它有几个方法可以呼叫:

    1. logger.debug 除错用的讯息,Production环境会忽
    2. logger.info 值得记录的一般讯
    3. logger.warn 值得记录的警告讯
    4. logger.error 错误讯息,但还不到网站无法执行的地
    5. logger.fatal 严重错误到网站无法执行的讯

    例如,你想要观察程序中变量@event的值,你可以插入以下程序到要观察的程序段落之中:

    Rails.logger.debug("event: #{@event.inspect}")

    开一个指令窗口执行tail -flog/development.log来观察log档案,接着开浏览器跑实际跑过这段程序,你就会在log/development.log看到除错讯息了。

    Production环境中,log/production.log会逐渐长大,可以使用 logrotate 定期整理 Rails Log 档案

    Rails也可以使用断点的除错方式,请编辑Gemfile打开以下的批注:

    # To use debugger (ruby-debug for Ruby 1.8.7+, ruby-debug19 for Ruby 1.9.2+)
    # gem 'ruby-debug'
    gem 'ruby-debug19', :require => 'ruby-debug'

    然后在要设定断点的地方呼叫debugger方法,你的服务器程序或Console就会在这里停下来让你检查。不过,会必须要用到这招的情形不多就是了。

    你可能感兴趣的:(编程技术)