手工打造CRUD应用程式
手工打造 CRUD 应用程式
Much of the essence of building a program is in fact the debugging of the specification. - Fred Brooks, The Mythical Man-Month 作者
请注意本章内容衔接后一章,请与后一章一起完成。
初入门像 Rails 这样的功能丰富的开发框架,难处就像鸡生蛋、蛋生鸡的问题:要了解运作的原理,你必须了解其中的元件,但是如果个别学习其中的元件,又将耗费许多的时间而见树不见林。因此,为了能够让各位读者能够尽快建构出一个基本的应用程式,有个大局观。我们将从一个 CRUD 程式开始。所谓的 CRUD 即为 Create 、 Read 、 Update 、 Delete 等四项基本数据库操作,本章将示范如何做出这个基本的应用程式,以及几项 Rails 常用功能。细节的原理说明则待 Part 2 后续章节。
Rails的MVC元件
我们在第一章 Ruby on Rails 简介有介绍了什么是 MVC 架构,而在 Rails 中分成几个不同元件来对应:
- ActiveRecord 是 Rails 的 Model 元件
- ActionPack 包含了 ActionDispatch 、 ActionController 和 ActionView ,分别是 Rails 的 Routing 、 Controller 和 View 元件
这张图示中的执行步骤是:
- 浏览器发出 HTTP request 请求给 Rails
- 路由(Routing)根据规则决定派往哪一个 Controller 的 Action
- 负责处理的 Controller Action 操作 Model 资料
- Model 存取数据库或资料处理
- Controller Action 将得到的资料餵给 View 样板
- 回传最后的 HTML 成品给浏览器其中,路由主要是根据 HTTP Method 方法(GET 、 POST 或是 PATCH 、 DELETE 等)以及网址来决定派往到哪一个 Controller 的 Action 。例如,我们在「Rails起步走」一章中的
get "welcome/say _hello " => "welcome#say"_意思就是,将_GET welcome/say_hello 的这个 HTTP request 请求,派往到 welcome controller 的 say action 。
认识ActiveRecord操作数据库
ActiveRecord 是 Rails 的 ORM(Object-relational mapping) 元件,负责与数据库沟通,让你可以使用物件导向语法来操作关联式数据库,它的对应概念如下:
- 将数据库表格 (table) 对应到一个类别 (class)
- 类别方法就是操作这个表格 (table) ,例如新增资料、多笔资料更新或多笔资料删除
- 资料表中的一笔资料 (row) 对应到一个物件 (object)
- 物件方法就是操作这笔资料,例如更新或删除这笔资料
- 资料表的字段 (column) 就是物件的属性 (object attribute)
所以说数据库里面的资料表,我们用一个 Model 类别表示。而其中的一笔资料,就是一个 Model 物件。
不了解关联式数据库的读者,推荐阅读MySQL 超新手入门从第0章至第5章CRUD与资料维护。
ActiveRecord 这个名称的由来是它使用了 Martin Fowler 的 Active Record 设计模式。
第三章「 Rails 起步走」我们提到了 Scaffold 鹰架功能,有经验的 Rails 程式设计师虽然不用鹰架产生程式码,不过还是会使用 Rails 的 generator 功能来分别产生 Model 和 Controller 档案。这里让我们来产生一个 Model :
$ rails g model event name:string description:text is_public:boolean capacity:integer
这些指令必须要在 Rails 专案目录下执行,承第三章也就是 demo 目录下。
接着执行以下指令就会建立资料表(如果是使用 SQLite3 数据库话,会产生 db/development.sqlite3 这个档案):
$ bin/rake db:migrate
接着,让我们使用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( :name => 'abc', :is_public => false )
## 删除
> e.destroy
和 irb 一样,要离开 rails console 请输入exit。如果输入的程式乱掉没作用时,直接Ctrl+Z离开也没关系。
对数据库好奇的朋友,可以安装 DB Browser for SQlite 这套工具,实际打开 db/development.sqlite3 这个档案观察看看。
认识Migration建立资料表
Rails 使用了 Migration 数据库迁移机制来定义数据库结构 (Schema) ,档案位于 db/migrate/ 目录下。它的目的在于:
- 让数据库的修改也可以纳入版本控制系统,所有的变更都透过撰写 Migration 档案执行
- 方便应用程式更新升级,例如让软件从第三版更新到第五版,数据库更新只需要执行
rake db:migrate - 跨数据库通用,不需修改程式就可以用在 SQLite3 、 MySQL 、 Postgres 等不同数据库
在上一节产生 Model 程式时, Rails 就会自动帮你产生对应的 Migration 档案,也就是如 db/migrate/20110519123430_create_events.rb 的档案。 Rails 会用时间戳章来命名档案,所以每次产生档名都不同,这样可以避免多人开发时的冲突。其内容如下:
## db/migrate/20110519123430_create_events.rb
class CreateEvents < ActiveRecord::Migration[5.1]
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 区块就是定义资料表结构的程式。上一节中我们已经执行过bin/rake db:migrate来建立此资料表。其中 timestamps 实际上会建立两个时间(datetime)字段:资料建立时间 created_at 和最后更新时间 updated_at,并且 Rails 会在资料存进数据库时,自动设定这两个字段的值。
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[5.1]
def change
add_column :events, :status, :string
end
end
接着执行bin/rake db:migrate就会在 events 表格中新增一个 status 的字段,字段型别是 string 。 Rails 会记录你已经对数据库操作过哪些 Migrations ,像此例中就只会跑这个 Migration 而已,就算你多执行几次bin/rake db:migrate也只会对数据库操作一次。
Rails 透过数据库中的 schema_migrations 这张 table 来记录已经跑过哪些 Migrations 。
认识ActiveRecord资料验证(Validation)
ActiveRecord 的资料验证 (Validation) 功能,可以帮助我们检查资料的正确性。如果验证失败,就会无法存进数据库。
编辑 app/models/event.rb 加入
class Event < ApplicationRecord
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应用程式
有了 Event model ,接下来让我们实作出完整的 CRUD 使用者接口流程吧,这包含了列表页面、新增页面、编辑页面以及个别资料页面。
外卡路由
我们在「 Rails 起步走」一章分别为 welcome/say_hello 和 welcome 设定路由,也就如何将网址对应到 Controller 和 Action 。但是如果每个路径都需要一条条设定会太麻烦了。这一章我们使用一种外卡路由的设定,编辑 config/routes.rb 在最后插入一行:
# ....
match ':controller(/:action(/:id(.:format)))', :via => :all
end
外卡路由很容易理解,它会将 /foo/bar 这样的网址自动对应到 Controller foo 的 bar Action 、如果是 /foo 这样的网址,则默认对应到 index action 。我们再下一章中我们会再改用另一种称作 RESTful 路由方式。
列出所有资料
执行 rails g controller events ,首先编辑 app/controllers/events_controller.rb 加入
def index
@events = Event.all
end
Event.all会抓出所有的资料,回传一个阵列给实例变量 (instance variables) 指派给@events。在 Rails 会让 Action 里的实例变量(也就是有@开头的变量)通通传到 View 样板里面可以使用。这个 Action 默认使用的样板是 app/views/events/ 目录下与 Action 同名的档案,也就是接下来要编辑的 app/views/events/index.html.erb ,内容如下:
<ul>
<% @events.each do |event| %>
<li>
<%= 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 %>
</li>
<% end %>
</ul>
<%= link_to 'New Event', :controller => 'events', :action => 'new' %>
这个 View 迭代了@events阵列并显示内容跟超连结,有几件值得注意的事情:
<%和<%=不太一样,前者只执行不输出,像用来迭代的each和end这两行就不需要输出。而后者<%= 里的结果会输出给浏览器。
link_to建立超连结到一个特定的位置,这里为浏览、编辑和删除都提供了超连结。
连往 <http://localhost:3000/events> 就会看到这一页。目前还没有任何资料,让我们继续实作点击 New Event 超连结之后的动作。
新增资料
建立一篇新的活动需要两个 Actions 。第一个是 new Action ,它用来实例化一个空的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_ 的程式码区块(Code block)被用来建立_HTML 表单。在区块中,你可以使用各种函式来建构表单。例如f.text _field :name_ 建立出一个文字输入框,并填入@event的_name_属性资料。但这个表单只能基于这个 _Model 有的属性(在这个例子是 name 跟 description)。 Rails 偏好使用form _for_ 而不是让你手写表单_HTML ,这是因为程式码可以更加简洁,而且可以明确地连结在 Model 物件上。
form _for_ 区块也很聪明,_New Event 的表单跟 Edit Event 的表单,其中的送出网址跟按钮文字会不同的(根据@event的不同,前者是新建的,后者是已经建立过的)。
如果你需要建立任意字段的 HTML 表单,而不绑在某一个 Model 上,你可以使用
form_tag函式。它也提供了建构表单的函式而不需要绑在 Model 实例上。我们会在 Action View: Helpers 一章介绍。
当一个使用者点击表单的 Create 按钮时,浏览器就会送出资料到 Controller 的 create Action 。也是一样编辑 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) 至 index Action 显示活动列表。
让我们来实际测试看看,在浏览器中实际按下表单的 Create 按钮后,出现了ActiveModel::ForbiddenAttributesError in EventsController#create的错误讯息,这是因为 Rails 会检查使用者传进来的参数必须经过一个过滤的安全步骤,这个机制叫做 Strong Parameters ,让我们回头修改 app/controllers/events_controller.rb
def create
@event = Event.new(event_params)
@event.save
redirect_to :action => :index
end
private
def event_params
params.require(:event).permit(:name, :description)
end
我们新加了一个event _params_ _方法,其中透过require和permit将params这个_Hash_过滤出params[:event][:name]和params[:event][:description]。
private以下的所有方法都会变成 private 方法,所以记得放在档案的最底下。
再次测试看看,应该就可以顺利新增资料了。
显示个别资料
当你在 index 页面点击 show 的活动连结,就会前往 <http://localhost:3000/events/show/1> 这个网址。 Rails 会呼叫 show action 并设定params[:id]为1。以下是 show Action :
编辑 app/controllers/events_controller.rb 加入
def show
@event = Event.find(params[:id])
end
这个 show Action 用find方法从数据库中找出该篇活动。找到资料之后, Rails 用 show.html.erb 样板显示出来。新增 app/views/events/show.html.erb ,内容如下:
<%= @event.name %>
<%= simple_format(@event.description) %>
<p><%= link_to 'Back to index', :controller => 'events', :action => 'index' %></p>
其中simple _format_ 是一个内建的_View Helper ,它的作用是可以将换行字符\n置换成<br />,有基本的 HTML 换行效果。
编辑资料
如同建立新活动,编辑活动也有两个步骤。第一个是请求特定一篇活动的 edit 页面。这会呼叫 Controller 的 edit Action ,编辑 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 很像,只是送出表单后,是前往 Controller 的 update Action :
def update
@event = Event.find(params[:id])
@event.update(event_params)
redirect_to :action => :show, :id => @event
end
在 update Action 里, Rails 一样透过params[:id]参数找到要编辑的资料。接着update方法会根据表单传进来的参数修改到资料上,这里我们沿用event _params_ 这个方法过滤使用者传进来的资料。如果一切正常,使用者会被导向到活动的_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 放到 Layout 的 HTML 之中。默认的 Layout 档案是 app/views/layouts/application.html.erb ,其中yield就是会被替换成样板的地方。所有的样版默认都会套这个 Layout 。我们会再 Action View 一章中介绍如何更换不同 Layout 。
现在,让我们修改 Layout 中的<title>:
<!DOCTYPE html>
<html>
<head>
<title><%= @page_title || "Event application" %></title>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
如此我们可以在 show Action 中设定@page_title的值:
def show
@event = Event.find(params[:id])
@page_title = @event.name
end
这样的话,进去 show 页面的 title 就会是活动名称。其他页面因为没有设定@page_title,就会是”Event application”。
认识局部样板(Partial Template)
利用局部样板 (Partial) 机制,我们可以将重复的样板独立出一个单独的档案,来让其他样板共享引用。例如 new.html.erb 和 edit.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_action方法
透过before _action_ ,我们可以将_Controller 中重复的程式独立出来。
在 events_controller.rb 开头内新增一行:
class EventsController < ApplicationController
before_action :set_event, :only => [ :show, :edit, :update, :destroy]
# ....
# ... 下略
在下方private后面新增一个方法如下:
def set_event
@event = Event.find(params[:id])
end
Controller 中的公开 (public) 方法都是 Action ,也就是可以让浏览器呼叫使用的动作。使用
protected或private可以避免内部方法被当做 Action 使用。
删除 show 、 edit 、 update 、 destroy 方法中的
@event = Event.find(params[:id])
加入资料验证
我们在资料验证一节中,已经加入了 name 的必填验证,因此当使用者送出没有 name 的表单,就会无法储存进数据库。我们希望目前的程式能够在验证失败后,提示使用者储存失败,并让使用者有机会可以修改再送出。
修改 app/controllers/events_controller.rb 的 create 和 update Action
def create
@event = Event.new(event_params)
if @event.save
redirect_to :action => :index
else
render :action => :new
end
end
如果活动因为验证错误而储存失败,这里会回传给使用者带有错误讯息的 new Action ,好让使用者可以修正问题再试一次。实际上,render :action => "new"会回传 new Action 所使用的样板,而不是执行 new action 这个方法。如果改成使用redirect _to_ 会让浏览器重新导向到_new Action ,但是如此一来@event就被重新建立而失去使用者刚输入的资料。
def update
if @event.update(event_params)
redirect_to :action => :show, :id => @event
else
render :action => :edit
end
end
更新时也是一样,如果验证有任何问题,它会显示 edit 页面好让使用者可以修正资料。
而为了可以在储存失败时显示错误讯息,接着编辑_form.html.erb中加入
<% if @event.errors.any? %>
<ul>
<% @event.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
<% end %>
认识Flash讯息
请在 app/views/layouts/application.html.erb__Layout 档案之中,yield之前加入:
<p style="color: green"><%= flash[:notice] %></p>
<p style="color: red"><%= flash[:alert] %></p>
接着让我们回到 app/controllers/events_controller.rb ,在 create Action 中加入
flash[:notice] = "event was successfully created"
在 update Action 中加入
flash[:notice] = "event was successfully updated"
在 destroy Action 中加入
flash[:alert] = "event was successfully deleted"
「event was successfully created」讯息会被储存在 Rails 的特殊 flash 变量中,好让讯息可以被带到另一个 action ,它提供使用者一些有用的资讯。在这个 create Action 中,使用者并没有真的看到任何页面,因为它马上就被导向到新的活动页面。而这个 flash 变量就带着讯息到下一个 Action ,好让使用者可以在 show Action 页面看到 「event was successfully created.」这个讯息。
分页外挂
上述的程式用Event.all一次抓出所有活动,这在资料量一大的时候非常浪费效能和内存。通常会用分页机制来限制抓取资料的笔数。
编辑 Gemfile 加入以下程式,这个档案设定了此应用程式使用哪些套件。这里我们使用 kaminari 这个分页套件:
gem "kaminari"
执行bundle install就会安装。装好后需要重新启动服务器才会加载。
修改 app/controllers/events_controller.rb 的 index Action 如下
def index
@events = Event.page(params[:page]).per(5)
end
编辑 app/views/events/index.html.erb ,加入
<%= paginate @events %>
连往 <http://localhost:3000/events/> ,你可能需要多加几笔资料就会看到分页连结了。
