第 2 章 玩具应用
本章我们要开发一个简单的演示应用,展示 Rails 强大的功能。我们会使用脚手架快速生成应用,这样就能站在一定高度上概览 Ruby on Rails 编程的过程(也能大致了解 Web 开发)。正如旁注 1.2 所说,本书将采用与众不同的方法,循序渐进开发一个完整的演示应用,遇到新的概念都会详细说明。不过为了快速概览(也为了寻找成就感),无需对脚手架避而不谈。我们将通过 URL 与最终开发出来的玩具应用交互,了解 Rails 应用的结构,也第一次演示 Rails 使用的 REST 架构。
与后面的演示应用类似,这个玩具应用中有用户(users)和微博(microposts),因此算是一个简化的 Twitter 类应用。应用的功能还需要后续开发,而且开发过程中的很多步骤看起来很神秘,不过暂时不用担心:从3.2 节起将从零开始再开发一个类似的完整应用,我还会提供大量的资料供你后续阅读。你要有些耐心,不要怕多犯错误,本章的主要目的就是让你不要被脚手架的神奇迷惑住,而要更深入地了解 Rails。
2.1 规划应用
这一节,我们要规划一下这个玩具应用。与 1.3 节一样,我们先使用 rails new
命令(指定 Rails 的版本号)生成应用的骨架:
$ cd ~/environment
$ rails _5.1.6_ new toy_app
$ cd toy_app/
如果使用1.2.1 节推荐的云端 IDE,这个应用可以在第一个应用所在的工作空间中创建,没必要再新建一个工作空间。如果没看到文件,可以点击文件浏览器中的齿轮图标,然后选择“Refresh File Tree”(刷新文件树)。
然后,在文本编辑器中修改 Gemfile
文件,写入代码清单 2.1 中的内容。
代码清单 2.1:这个玩具应用的 Gemfile
文件
source 'https://rubygems.org'
gem 'rails', '5.1.6'
gem 'puma', '3.9.1'
gem 'sass-rails', '5.0.6'
gem 'uglifier', '3.2.0'
gem 'coffee-rails', '4.2.2'
gem 'jquery-rails', '4.3.1'
gem 'turbolinks', '5.0.1'
gem 'jbuilder', '2.7.0'
group :development, :test do
gem 'sqlite3', '1.3.13'
gem 'byebug', '9.0.6', platform: :mri
end
group :development do
gem 'web-console', '3.5.1'
gem 'listen', '3.1.5'
gem 'spring', '2.0.2'
gem 'spring-watcher-listen', '2.0.1'
end
group :production do
gem 'pg', '0.20.0'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
与 1.5.1 节一样,安装 gem 时要指定 --without production
选项,不安装生产环境使用的 gem:
$ bundle install --without production
如 1.3.1 节所述,可能还要运行 bundle update
(旁注 1.1)。
最后,把这个玩具应用纳入 Git 版本控制系统:
$ git init
$ git add -A
$ git commit -m "Initialize repository"

你还可以在 Bitbucket 中点击“Create”(新建)按钮创建一个新仓库(图 2.1),然后把代码推送到这个远程仓库中:
$ git remote add origin git@bitbucket.org:<username>/toy_app.git
$ git push -u origin --all
越早部署应用越好。我建议你按照 1.3.4 节所述的步骤做,修改代码清单 2.2 和代码清单 2.3。
代码清单 2.2:在 Application
控制器中添加 hello
动作
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
def hello
render html: "hello, world!"
end
end
代码清单 2.3:设置根路由
Rails.application.routes.draw do
root 'application#hello'
end
然后,提交改动,再推送到 Heroku 中:
$ git commit -am "Add hello"
$ heroku create
$ git push heroku master
(与 1.5 节一样,你可能会看到一些提醒消息,现在先不去管它。7.5 节会解决。)除了 Heroku 为应用提供的地址之外,输出的内容应该与图 1.24 一样。
下面要开发这个应用了。一般来说,开发 Web 应用的第一步是创建数据模型(data model)。模型表示应用所需的结构。这个玩具应用是个Twitter 类微博,只有用户和简短的文章(微博)。那么,我们先为这个应用添加 User
模型(2.1.1 节),然后再添加 Micropost
模型(2.1.2 节)。
2.1.1 User
模型
网络中有多少不同的注册表单,就有多少定义用户数据模型的方式。简单起见,我们将使用一种最简可用的方式。这个玩具应用的用户有一个唯一的标识 id
(integer
类型)、一个公开的名字 name
(string
类型)和一个电子邮件地址 email
(也是 string
类型)。电子邮件地址将作为用户名使用。User
模型的结构如图 2.2。

User
数据模型在 6.1.1 节会看到,图 2.2 中的 users
对应于数据库中的一个表(table);id
、name
和 email
是表中的列(column)。
2.1.2 Micropost
模型
Micropost
数据模型比 User
模型还要简单:微博只要一个 id
和表示微博内容的 content
(text
类型)字段即可。[1]此外还有一个比较复杂的字段要实现,这个字段把微博和用户关联(associate)起来。我们使用 user_id
存储微博的属主。最终得到的 Micropost
数据模型如图 2.3 所示。

Micropost
数据模型2.2 Users 资源
这一节我们要实现 2.1.1 节设定的 User
数据模型,还会为它创建 Web 界面。二者结合起来就是一个 Users
资源。“资源”的意思是把用户设想为对象,可以通过 HTTP 协议在网页中创建(create)、读取(read)、更新(update)和删除(delete)。正如前面提到的,我们将使用 Rails 内置的脚手架生成 Users
资源。我建议你先不要细看脚手架生成的代码,这时看只会让你更困惑。
把 scaffold
传给 rails generate
命令就可以使用 Rails 的脚手架了。传给 scaffold
的参数是资源名的单数形式(这里是 User
)[2],后面可以再跟着一些可选参数,指定数据模型中的字段:
$ rails generate scaffold User name:string email:string
invoke active_record
create db/migrate/20160515001017_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
route resources :users
invoke scaffold_controller
create app/controllers/users_controller.rb
invoke erb
create app/views/users
create app/views/users/index.html.erb
create app/views/users/edit.html.erb
create app/views/users/show.html.erb
create app/views/users/new.html.erb
create app/views/users/_form.html.erb
invoke test_unit
create test/controllers/users_controller_test.rb
invoke helper
create app/helpers/users_helper.rb
invoke test_unit
invoke jbuilder
create app/views/users/index.json.jbuilder
create app/views/users/show.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
我们在执行的命令中加入了 name:string
和 email:string
,这样就可以实现图 2.2 中的 User
模型了。注意,没必要指定 id
字段,Rails 会自动创建并将其设为表的主键(primary key)。
接下来我们要用 rails db:migrate
命令迁移(migrate)数据库,如代码清单 2.4 所示。
代码清单 2.4:迁移数据库
$ rails db:migrate
== CreateUsers: migrating ====================================================
-- create_table(:users)
-> 0.0017s
== CreateUsers: migrated (0.0018s) ===========================================
上述命令的作用是使用新的 User
数据模型更新数据库。(从 6.1.1 节开始会深入学习数据库迁移。)
顺便说一下,在 Rails 5 之前的版本中,db:migrate
命令使用 rake
执行,而不是 rails
。因此,如果你还要维护以前的应用,一定要知道如何使用 Rake(旁注 2.1)。
执行代码清单 2.4 中的迁移之后,可以新打开一个终端标签页(图 1.10),运行本地 Web 服务器:
$ rails server
现在,这个玩具应用应该可以在本地服务器中访问了,结果与 1.3.2 节一样。(如果使用云端 IDE,要在一个新的浏览器选项卡中打开网页,别在 IDE 中打开。)
2.2.1 浏览用户相关的页面
访问根 URL,我们会看到与图 1.15 一样的“hello, world!”页面。不过使用脚手架生成 Users
资源时生成了很多用来处理用户的页面。例如,列出所有用户的页面 /users,创建新用户的页面 /users/new。本节的目的是走马观花地浏览一下这些用户相关的页面。浏览时你会发现表 2.1 很有用,表中显示了页面和 URL 之间的对应关系。
URL | 动作 | 作用 |
---|---|---|
/users |
|
列出所有用户 |
/users/1 |
|
显示 ID 为 1 的用户 |
/users/new |
|
创建新用户 |
/users/1/edit |
|
编辑 ID 为 1 的用户 |
我们先来看显示应用中所有用户的页面,这个页面叫索引页,路径是 /users。和预期一样,目前还没有用户,如图 2.4 所示。
如果想创建新用户,要访问 /users/new 路径上的页面,如图 2.5 所示。第 7 章会把这个页面打造成用户注册页面。
我们可以在表单中填入名字和电子邮件地址,然后点击“Create User”(创建用户)按钮创建一个用户。
此时,浏览器会转向这个用户的页面,即 /users/1,如图 2.6 所示。(页面中显示的绿色文字是闪现消息(flash message),7.4.2 节会介绍。)注意,这个页面的 URL 是 /users/1。你可能猜到了,这里的 1
就是图 2.2 中的用户 id
。7.1 节会把这个页面打造成用户的资料页。
如果想修改用户的信息,要访问编辑页面,即 /users/1/edit(图 2.7)。修改用户信息后点击“Update User”(更新用户)按钮就更改了这个玩具应用中该用户的信息(图 2.8)。(第 6 章会详细介绍,用户的信息存储在后端的数据库中。)我们会在 10.1 节为演示应用添加编辑和更新用户信息的功能。

Users
资源的索引页(/users)




现在回到 /users/new 页面,在表单中填写信息,创建第二个用户。然后访问用户索引页,结果如图 2.9 所示。7.1 节会美化这个显示所有用户的页面。
我们已经看了创建、显示和编辑用户的页面,最后要看删除用户的页面(图 2.10)。点击图 2.10 中所示的链接后,会删除第二个用户,现在索引页面就只剩一个用户了。(如果这个操作不成功,确认浏览器是否启用了 JavaScript。Rails 通过 JavaScript 发送删除用户的请求。)10.4 节会为演示应用实现用户删除功能,而且仅限于管理员级别的用户才能执行这项操作。

练习
-
(如果你了解 CSS)创建一个新用户,然后使用浏览器中的 HTML 审查工具找出“User was successfully created.”文本的 CSS ID。刷新页面后会发生什么?
-
如果创建用户时只填写名字,而没填写电子邮件地址,会发生什么?
-
如果创建用户时填写的电子邮件地址无效,例如填写的是“@example.com”,会发生什么?
-
删除前几题创建的用户。删除用户时,Rails 会显示消息吗?
2.2.2 MVC 实战
我们已经快速概览了 Users
资源,下面我们从 MVC(1.3.3 节)的视角出发,审视其中某些部分。我们将分析在浏览器中访问用户索引页(/users)的过程,了解一下 MVC(图 2.11)。

图中各步的说明如下:
-
浏览器向 /users 发送请求;
-
Rails 的路由把 /users 交给
Users
控制器的index
动作处理; -
index
动作要求User
模型检索所有用户(User.all
); -
User
模型从数据库中读取所有用户; -
User
模型把所有用户组成的列表返回给控制器; -
控制器把所有用户赋值给
@users
变量,然后传入index
视图; -
视图使用嵌入式 Ruby 把页面渲染成 HTML;
-
控制器把 HTML 送回浏览器。[3]
下面详细分析这个过程。首先,浏览器发送请求(第 1 步)。这一步可以直接在浏览器地址栏中输入地址,也可以点击网页中的链接。请求到达 Rails 路由器(第 2 步),路由器根据 URL(以及请求的类型,参见旁注 3.2)把请求分配给合适的控制器动作。把 Users
资源中相关的 URL 映射到控制器动作的代码如代码清单 2.5 所示。那行代码会按照表 2.1 中的对应关系做映射。(:users
这个写法看着很奇怪,它是一个符号(Symbol),4.3.3 节会介绍。)
代码清单 2.5:Rails 路由,为 Users
资源定义了一条规则
Rails.application.routes.draw do
resources :users
root 'application#hello'
end
既然打开路由文件了,那就花点儿时间把根路由改为用户索引页吧。修改之后,访问根地址就会显示 /users 页面。我们在代码清单 2.3 中添加了根路由:
root 'application#hello'
上述规则把根路由指向 Application
控制器中的 hello
动作。现在,我们想使用 Users
控制器的 index
动作,因此要按照代码清单 2.6 所示的代码修改。
代码清单 2.6:把根路由指向 Users
控制器的动作
Rails.application.routes.draw do
resources :users
root 'users#index'
end
一个控制器中有多个动作,2.2.1 节浏览的页面对应于 Users
控制器的不同动作。脚手架生成的控制器代码摘要如代码清单 2.7 所示。注意 class UsersController < ApplicationController
这种写法,在 Ruby 中这表示类继承。(2.3.4 节会简要介绍继承,4.4 节再做详细介绍。)
代码清单 2.7:Users
控制器代码摘要
class UsersController < ApplicationController
.
.
.
def index
.
.
.
end
def show
.
.
.
end
def new
.
.
.
end
def edit
.
.
.
end
def create
.
.
.
end
def update
.
.
.
end
def destroy
.
.
.
end
end
你可能注意到了,动作的数量比我们看过的页面数量多,index
、show
、new
和 edit
对应于 2.2.1 节介绍的页面。此外还有一些其他动作,create
、update
和 destroy
。这些动作一般不直接渲染页面(不过有时也会),只会修改数据库中保存的用户数据。表 2.2 列出了控制器的全部动作,这些动作就是 Rails 对 REST 架构(旁注 2.2)的实现。REST 架构由计算机科学家 Roy Fielding 提出,意思是“表现层状态转化”(Representational State Transfer)。[4]注意表 2.2 中的内容,有些部分有重叠。例如 show
和 update
两个动作都映射到 /users/1 这个地址上,二者的区别是响应的 HTTP 请求方法不同。3.3 节会更详细地介绍 HTTP 请求方法。
HTTP 请求 | URL | 动作 | 作用 |
---|---|---|---|
|
/users |
|
列出所有用户 |
|
/users/1 |
|
显示 ID 为 1 的用户 |
|
/users/new |
|
显示创建新用户的页面 |
|
/users |
|
创建新用户 |
|
/users/1/edit |
|
显示 ID 为 1 的用户的编辑页面 |
|
/users/1 |
|
更新 ID 为 1 的用户 |
|
/users/1 |
|
删除 ID 为 1 的用户 |
为了探明 Users
控制器与 User
模型之间的关系,我们看一下简化后的 index
动作,如代码清单 2.8 所示。(要阅读不完全能理解的代码也体现了“技术是复杂的”。)
代码清单 2.8:这个玩具应用中简化的 index
动作
class UsersController < ApplicationController
.
.
.
def index
@users = User.all
end
.
.
.
end
index
动作中有一行代码,@users = User.all
(图 2.11 中的第 3 步),让 User
模型从数据库中检索所有用户(第 4 步),然后把结果赋值给 @users
变量(读作“at-users”,第 5 步)。User
模型的代码参见代码清单 2.9。这段代码看似简单,但是通过继承具备了很多功能(参见 2.3.4 节 和 4.4 节)。具体而言,使用 Rails 中名为 Active Record 的库后,User.all
就能返回数据库中的所有用户。
代码清单 2.9:玩具应用中的 User
模型
class User < ActiveRecord::Base
end
定义 @users
变量后,控制器再调用视图(第 6 步)。视图的代码如代码清单 2.10 所示。以 @
开头的变量是实例变量(instance variable),在视图中自动可用。这里,index.html.erb
视图中的代码(代码清单 2.10)遍历 @users
,为每个用户生成一行 HTML。(你现在可能读不懂这些代码,这里只是让你看一下视图是什么样子。)
代码清单 2.10:用户索引页的视图
<h1>Listing users</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th colspan="3"></th>
</tr>
</thead>
<% @users.each do |user| %>
<tr>
<td><%= user.name %></td>
<td><%= user.email %></td>
<td><%= link_to 'Show', user %></td>
<td><%= link_to 'Edit', edit_user_path(user) %></td>
<td><%= link_to 'Destroy', user, method: :delete,
data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</table>
<br>
<%= link_to 'New User', new_user_path %>
视图把代码转换成 HTML(第 7 步),然后控制器将其返回给浏览器,再显示出来(第 8 步)。
练习
-
参照图 2.11,写出访问 /users/1/edit 页面的步骤。
-
在脚手架生成的代码中找出前一题从数据库中检索用户的代码。
-
编辑用户页面的视图文件,其名称是什么?
2.2.3 这个 Users
资源的不足
脚手架生成的 Users
资源虽然能够让你大致了解 Rails,但也有一些不足:
-
没有验证数据。
User
模型会接受空名字和无效的电子邮件地址,而不报错。 -
没有验证身份。没实现登录和退出功能,随意一个用户都可以进行任何操作。
-
没有测试。也不是完全没有,脚手架会生成一些基本的测试,不过很粗糙也不灵便,没有针对数据验证和身份验证的测试,更别说针对其他功能的测试了。
-
没样式,没布局。没有共用的样式和网站导航。
-
没真正理解。如果你能读懂脚手架生成的代码,就不需要阅读这本书了。
2.3 Microposts
资源
我们已经生成并浏览了 Users
资源,现在要生成 Microposts
资源。阅读本节时,我推荐你和 2.2 节对比一下。你会发现这两个资源在很多方面都是一致的。通过这样重复生成资源,我们可以更好地理解 Rails 中的 REST 架构。在这样的早期阶段看一下 Users
资源和 Microposts
资源的相同之处,也是本章的主要目的之一。
2.3.1 概览 Microposts
资源
与 Users
资源一样,我们将使用 rails generate scaffold
命令生成 Microposts
资源的代码,不过这一次要实现图 2.3 中的数据模型:[5]
$ rails generate scaffold Micropost content:text user_id:integer
invoke active_record
create db/migrate/20160515211229_create_microposts.rb
create app/models/micropost.rb
invoke test_unit
create test/models/micropost_test.rb
create test/fixtures/microposts.yml
invoke resource_route
route resources :microposts
invoke scaffold_controller
create app/controllers/microposts_controller.rb
invoke erb
create app/views/microposts
create app/views/microposts/index.html.erb
create app/views/microposts/edit.html.erb
create app/views/microposts/show.html.erb
create app/views/microposts/new.html.erb
create app/views/microposts/_form.html.erb
invoke test_unit
create test/controllers/microposts_controller_test.rb
invoke helper
create app/helpers/microposts_helper.rb
invoke test_unit
invoke jbuilder
create app/views/microposts/index.json.jbuilder
create app/views/microposts/show.json.jbuilder
invoke assets
invoke coffee
create app/assets/javascripts/microposts.coffee
invoke scss
create app/assets/stylesheets/microposts.scss
invoke scss
identical app/assets/stylesheets/scaffolds.scss
(如果看到 Spring 相关的错误,再次执行这个命令即可。)然后,跟 2.2 节一样,我们要执行迁移,更新数据库,使用新建的数据模型:
$ rails db:migrate
== CreateMicroposts: migrating ===============================================
-- create_table(:microposts)
-> 0.0023s
== CreateMicroposts: migrated (0.0026s) ======================================
现在我们就可以使用类似 2.2.1 节中介绍的方法来创建微博了。你可能猜到了,脚手架还会更新 Rails 的路由文件,为 Microposts
资源加入一条规则,如代码清单 2.11 所示。[6]与 Users
资源类似,resources :micropsts
把微博相关的 URL 映射到 Microposts
控制器上,如表 2.3 所示。
代码清单 2.11:Rails 的路由,有一条针对 Microposts
资源的新规则
Rails.application.routes.draw do
resources :microposts
resources :users
root 'users#index'
end
HTTP 请求 | URL | 动作 | 作用 |
---|---|---|---|
|
/microposts |
|
列出所有微博 |
|
/microposts/1 |
|
显示 ID 为 1 的微博 |
|
/microposts/new |
|
显示创建新微博的页面 |
|
/microposts |
|
创建新微博 |
|
/microposts/1/edit |
|
显示 ID 为 1 的微博的编辑页面 |
|
/microposts/1 |
|
更新 ID 为 1 的微博 |
|
/microposts/1 |
|
删除 ID 为 1 的微博 |
Microposts
控制器的代码简化后如代码清单 2.12 所示。注意,除了把 UsersController
换成 MicropostsController
之外,这段代码和代码清单 2.7 没什么区别。这说明了两个资源在 REST 架构中的共同之处。
代码清单 2.12:简化后的 Microposts
控制器
class MicropostsController < ApplicationController
.
.
.
def index
.
.
.
end
def show
.
.
.
end
def new
.
.
.
end
def edit
.
.
.
end
def create
.
.
.
end
def update
.
.
.
end
def destroy
.
.
.
end
end
我们在发布微博的页面(/microposts/new)输入一些内容,发布一篇微博,如图 2.12 所示。
既然已经打开这个页面了,那就多发布几篇微博,并且确保至少把一篇微博的 user_id
设为 1
,把微博赋予 2.2.1 节中创建的第一个用户。结果应该和图 2.13 类似。


练习
-
(如果你了解 CSS)发布一篇新微博,然后使用浏览器中的 HTML 审查工具找出“Micropost was successfully created.”文本的 CSS ID。刷新页面后会发生什么?
-
发布微博时不输入内容也不指定用户 ID 试试。
-
发布一篇内容超过 140 字(例如维基百科中介绍 Ruby 的第一段)的微博试试。
-
删除前几题创建的微博。
2.3.2 限制微博的长度
为了称得上“微博”这个名字,内容的长度要做限制。在 Rails 中实现这种限制很简单,使用验证(validation)功能即可。要限制微博的长度最多为 140 个字符(就像 Twitter 一样),我们可以使用长度验证。在文本编辑器或 IDE 中打开 app/models/micropost.rb
文件,写入代码清单 2.13 中的代码。
代码清单 2.13:限制微博的长度最多为 140 个字符
class Micropost < ApplicationRecord
validates :content, length: { maximum: 140 }
end
这段代码看起来可能很神秘,我们会在 6.2 节详细介绍验证。如果我们在发布微博的页面输入超过 140 个字符的内容,就能看到这个验证的作用了。如图 2.14 所示,Rails 会渲染错误消息,提示微博的内容太长了。(7.3.3 节会详细介绍错误消息。)

练习
-
使用前一节练习中的那段文字发布微博,这一次有什么变化呢?
-
使用浏览器中的 HTML 审查工具找到前一题那个错误消息的 CSS ID。
2.3.3 一个用户拥有多篇微博
Rails 最强大的功能之一,是可以在不同的数据模型之间建立关联(association)。对这里的 User
模型而言,每个用户可以拥有多篇微博。我们可以更新 User
模型(参见代码清单 2.14)和 Micropost
模型(参见代码清单 2.15)的代码实现这种关联。
代码清单 2.14:一个用户拥有多篇微博
class User < ApplicationRecord
has_many :microposts
end
代码清单 2.15:一篇微博属于一个用户
class Micropost < ApplicationRecord
belongs_to :user
validates :content, length: { maximum: 140 }
end
我们可以把这种关联用图 2.15 表示出来。因为 microposts
表中有 user_id
这一列,所以 Rails(通过 Active Record)能把微博和各个用户关联起来。

在第 13 章和第 14 章,我们会使用微博和用户之间的关联显示用户的所有微博,还会生成一个和 Twitter 类似的微博列表。现在,我们可以在控制台(console)中检查用户与微博之间的关联。控制台是与 Rails 应用交互常用的工具。在命令行中执行 rails console
命令,启动控制台。然后输入 User.first
,从数据库中检索第一个用户,并把得到的数据赋值给 first_user
变量:[7]
$ rails console
>> first_user = User.first
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.org",
created_at: "2016-05-15 02:01:31", updated_at: "2016-05-15 02:01:31">
>> first_user.microposts
=> [#<Micropost id: 1, content: "First micropost!", user_id: 1, created_at:
"2016-05-15 02:37:37", updated_at: "2016-05-15 02:37:37">, #<Micropost id: 2,
content: "Second micropost", user_id: 1, created_at: "2016-05-15 02:38:54",
updated_at: "2016-05-15 02:38:54">]
>> micropost = first_user.microposts.first # 使用 Micropost.first 也可以
=> #<Micropost id: 1, content: "First micropost!", user_id: 1, created_at:
"2016-05-15 02:37:37", updated_at: "2016-05-15 02:37:37">
>> micropost.user
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.org",
created_at: "2016-05-15 02:01:31", updated_at: "2016-05-15 02:01:31">
>> exit
(我在这段代码的最后一行加上了 exit
,告诉你如何退出控制台。在大多数系统中也可以按 Ctrl-D 键退出控制台。)[8]我们使用 first_user.microposts
获取这个用户发布的微博。Active Record 会自动返回 user_id
的值与 first_user
的 ID(1
)相同的所有微博。在第 13 章和第 14 章中,我们会更深入地学习关联。
练习
-
编辑显示用户的页面,显示用户发布的第一篇微博。(根据文件中的其他内容猜测所需的句法。)访问 /users/1,确认改动是正确的。
-
把代码清单 2.17 中的
FILL_IN
换成相应的代码,为User
模型的name
和email
属性添加存在性验证。效果如图 2.17 所示。
代码清单 2.16:验证微博内容存在性的代码
class Micropost < ApplicationRecord
belongs_to :user
validates :content, length: { maximum: 140 },
presence: true
end
代码清单 2.17:为 User
模型添加存在性验证
class User < ApplicationRecord
has_many :microposts
validates FILL_IN, presence: true # 把 FILL_IN 换成正确的代码
validates FILL_IN, presence: true # 把 FILL_IN 换成正确的代码
end


User
模型存在性验证的效果2.3.4 继承体系
接下来简要介绍 Rails 中控制器和模型的类继承。 如果你有面向对象编程(Object-oriented Programming,简称 OOP)的经验,尤其是类,能更好地理解这些内容。如果暂时不理解,也没关系,4.4 节会详细说明这些概念。
我们先介绍模型的继承体系。对比一下代码清单 2.18 和代码清单 2.19,可以看出,User
和 Micropost
都(通过 <
符号)继承自 ApplicationRecord
类,而这个类继承自 ActiveRecord::Base
类,这是 Active Record 为模型提供的基类。图 2.18 列出了这种继承关系。继承 ActiveRecord::Base
类,模型对象才能与数据库通讯,才能把数据库中的列看做 Ruby 中的属性,等等。
代码清单 2.18:User
类的继承关系
class User < ApplicationRecord
.
.
.
end
代码清单 2.19:Mcropost
类的继承关系
class Micropost < ApplicationRecord
.
.
.
end

User
模型和 Micropost
模型的继承体系控制器的继承结构与模型基本相同。对比代码清单 2.20 和代码清单 2.21,可以看出,UsersController
和 MicropostsController
都继承自 ApplicationController
。如代码清单 2.22 所示,ApplicationController
继承自 ActionController::Base
。ActionController::Base
是 Rails 中 Action Pack 库为控制器提供的基类。这些类之间的关系如图 2.19 所示。
代码清单 2.20:UsersController
类中的继承
class UsersController < ApplicationController
.
.
.
end
代码清单 2.21:MicropostsController
类中的继承
class MicropostsController < ApplicationController
.
.
.
end
代码清单 2.22:ApplicationController
类中的继承
class ApplicationController < ActionController::Base
.
.
.
end

UsersController
和 MicropostsController
的继承体系与模型的继承类似,通过继承 ActionController::Base
,Users
控制器和 Microposts
控制器获得了很多功能。例如,处理模型对象的能力、过滤入站 HTTP 请求,以及把视图渲染成 HTML 的能力。Rails 应用中的所有控制器都继承自 ApplicationController
,所以其中定义的规则会自动运用于应用中的每个动作。例如,9.1 节会介绍如何在 Application
控制器中引入辅助方法,为整个应用的所有控制器都加上登录和退出功能。
练习
-
查看
Application
控制器文件的内容,找出ApplicationController
继承自ActionController::Base
的代码。 -
ApplicationRecord
是不是也在类似的文件中继承ActiveRecord::Base
?提示:可能是app/models
目录中名为application_record.rb
的文件。
2.3.5 部署这个玩具应用
完成 Microposts
资源之后,是时候把代码推送到 Bitbucket 的仓库中了:
$ git status
$ git add -A
$ git commit -m "Finish toy app"
$ git push
通常情况下,你应该经常做一些很小的提交,不过对于本章来说,最后做一次大提交也无妨。
然后,你也可以参照 1.5 节所述的步骤,把这个应用部署到 Heroku 中:
$ git push heroku
(执行这个命令之前要按照 2.1 节中的说明创建 Heroku 应用:先执行 heroku create
命令,然后再执行 git push heroku master
命令。)
为了让应用能使用数据库,还要迁移生产数据库,方法是在代码清单 2.4 中那个迁移命令前面加上 heroku run
:
$ heroku run rails db:migrate

这个命令会按照 User
和 Micropost
数据模型更新 Heroku 中的数据库。迁移数据库之后,就可以在生产环境中使用这个应用了,如图 2.20 所示,而且这个应用使用 PostgreSQL 数据库。[9]
最后,如果你做了 2.3.3 节的练习,要把显示第一个用户微博的代码去掉,这样应用才能正确加载。你只需把那些代码删掉,做次提交,然后再推送到 Heroku。
练习
-
在线上应用中创建几个用户。
-
为第一个用户创建几篇微博。
-
发布微博时填写超过 140 个字,确认代码清单 2.13 中的验证在生产环境中可用。
2.4 小结
至此,对这个 Rails 应用的概览结束了。本章开发的玩具应用有优点也有缺点。
优点
-
概览了 Rails
-
介绍了 MVC
-
第一次体验了 REST 架构
-
开始使用数据模型了
-
在生产环境中运行了一个基于数据库的 Web 应用
缺点
-
没自定义布局和样式
-
没有静态页面(例如首页和“关于”页面)
-
没有用户密码
-
没有用户头像
-
没有登录功能
-
不安全
-
没实现用户和微博之间的自动关联
-
没实现“关注”和“被关注”功能
-
没实现微博列表
-
没编写有意义的测试
-
没有真正理解所做的事情
本书后续的内容建立在这些优点之上,而且会改善缺点。
2.4.1 本章所学
-
使用脚手架自动生成模型的代码,然后通过 Web 界面与应用交互;
-
脚手架利于快速上手,但生成的代码不易理解;
-
Rails 使用“模型-视图-控制器”(MVC)模式组织 Web 应用;
-
借由 Rails 我们得知,为了与数据模型交互,REST 架构制定了一套标准的 URL 和控制器动作;
-
Rails 支持数据验证,用于约束数据模型的属性可以使用什么值;
-
Rails 内建支持建立数据模型关联的功能;
-
可以使用 Rails 控制台在命令行中与 Rails 应用交互。