Ruby on Rails 教程

Ruby on Rails Tutorial 原书第 2 版(涵盖 Rails 4)

第 2 章 演示程序

  1. 2.1 规划程序
  2. 2.1.1 用户模型
  3. 2.1.2 微博模型
  4. 2.2 Users 资源(users resource)
  5. 2.2.1 浏览用户相关的页面
  6. 2.2.2 MVC 实践
  7. 2.2.3 上述 Users 资源的缺陷
  8. 2.3 Microposts 资源
  9. 2.3.1 概览 Microposts 资源
  10. 2.3.2 限制微博内容的长度
  11. 2.3.3 一个用户有多篇微博
  12. 2.3.4 继承关系
  13. 2.3.5 部署演示程序
  14. 2.4 小结

本章我们要开发一个简单的演示应用程序来展示一下 Rails 强大的功能。我们会使用脚手架(scaffold)功能快速的生成程序,这样就能以一定的高度概览一下 Ruby on Rails 编程的过程(也能大致的了解一下 Web 开发)。正如在第一章的旁注 1.1 中所说,本书将采用另一种方法,我们会循序渐进的开发程序,遇到新的概念都会详细说明,不过为了概览功能(也为了寻找成就感)也无需对脚手架避而不谈。我们可以通过 URL 和最终的演示程序进行交互,了解一下 Rails 应用程序的结构,也第一次演示 Rails 使用的 REST 架构。

和后面的大型示例程序类似,这个演示程序将包含用户(users)和微博(microposts)两个模型(因此实现了一个小型的 Twitter 类程序)。程序的功能还需要后续的开发,而且开发过程中的很多步骤看起来也很神秘,不过暂时不用担心:从第三章起将从零开始再开发一个类似的程序,我还会提供大量的资料供后续参考。你要有些耐心,不要怕多犯错误,本章的主要目的就是让你不要被脚手架的神奇迷惑住了,而要更深入的了解 Rails。

2.1 规划程序

在这一节我们要规划一下这个演示程序。和 1.2.3 节类似,我们先使用 rails 命令生成程序的骨架。

$ cd ~/rails_projects
$ rails new demo_app
$ cd demo_app

然后我们用一个文本编辑器修改 Gemfile,写入代码 2.1 所示的代码。

代码 2.1:演示程序的 Gemfile

source 'https://rubygems.org'
ruby '2.0.0'

gem 'rails', '4.0.4'

group :development do
  gem 'sqlite3', '1.3.8'
end

gem 'sass-rails', '4.0.1'
gem 'uglifier', '2.1.1'
gem 'coffee-rails', '4.0.1'
gem 'jquery-rails', '2.2.1'
gem 'turbolinks', '1.1.1'
gem 'jbuilder', '1.0.2'

group :doc do
  gem 'sdoc', '0.3.20', require: false
end

group :production do
  gem 'pg', '0.15.1'
end

注意,代码 2.1 和代码 1.9 是一样的。

1.4.1 节一样,在本地安装 gem 时指定 --without production 选项不安装生产环境所需的 gem:

$ bundle install --without production
$ bundle update
$ bundle install

(再次提醒,如果 Bundler 提示一个和 readline 有关的错误,请在 Gemfile 中加入 gem rb-readline。)

create demo repo 40

图 2.1:为演示程序在 GitHub 新建一个仓库

最后我们还要把演示程序纳入版本控制。提醒一下,rails 命令会生成一个默认的 .gitignore 文件,不过对于你所使用的系统而言代码 1.7 中的代码似乎更有用。然后初始化一个 Git 仓库,做第一次提交:

$ git init
$ git add .
$ git commit -m "Initial commit"

你可以重新创建一个仓库然后将代码推送到 GitHub:

$ git remote add origin https://github.com/<username>/demo_app.git
$ git push -u origin master

(和第一章中的程序一样,注意不要使用 GitHub 自动生成的 README 文件初始化仓库。)

下面要开发这个程序了。开发 Web 应用程序一般来说第一步是创建数据模型(data model),模型代表应用程序所需的结构。对我们这个程序而言,它是个轻博客,有用户和微博。那么我们先为程序创建一个用户(users)模型(2.1.1 节),然后再添加微博(microposts)模型(2.1.2 节)。

2.1.1 用户模型

不同的注册表单代表了不同的用户数据模型,我们将选择一种简化的模型。这个演示程序的用户要有一个唯一的标识符 id(整数 integer),一个对外显示的名字 name(字符串 string),还有一个 Email 地址 email(字符串 string),它将同时兼任用户名。用户模型的结构如图 2.2。

demo user model

图 2.2:用户数据模型

我们会在 6.1.1 节中介绍,图 2.2 中的标签 users 代表数据库中的一个表,idnameemail 是表中的列。

2.1.2 微博模型

微博数据模型的核心比用户的模型还要简单:微博要有一个 id 和一个内容 content(字符串 string)。1 不过还有一个比较复杂的数据要实现:将微博和用户关联起来,我们使用 user_id 来存储微博的拥有者。最终的数据模型如图 2.3。

demo micropost model

图 2.3:微博的数据模型

2.3.3 节中我们会看到怎样使用 user_id 字段简单的实现一个用户拥有多个微博的功能(第 10 章会做更详尽的介绍)。

2.2 Users 资源(users resource)

本节我们将要实现 2.1.1 节中设定的用户数据模型,还会为这个模型创建基于网页的界面。这二者结合起来就是一个“Users 资源”,“资源”的意思是将用户设想为对象,可以通过 HTTP 协议在网页中创建(create)、读取(read)、更新(update)和删除(delete)。正如前面提到的,我们的 Users 资源会使用脚手架功能生成,Rails 内置了这样的功能。我强烈建议你先不要细看生成的代码,在这个时候看只会让你更困惑。

scaffold 传递给 rails generate 就可以使用 Rails 的脚手架功能了。传给 scaffold 的参数是资源名的单数形式(本例中就是 User),后面可以再跟着指定数据模型的字段:2

$ rails generate scaffold User name:string email:string
      invoke  active_record
      create    db/migrate/20130305221714_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  jbuilder_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
      create        test/helpers/users_helper_test.rb
      invoke    jbuilder
       exist      app/views/users
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/users.js.coffee
      invoke    scss
      create      app/assets/stylesheets/users.css.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.css.scss

上面代码中的命令加入了 name:stringemail:string,这样我们就可以实现如图 2.2 所示的用户模型了。(注意没必要指定 id,Rails 会自动创建并将其设为表的主键(primary key)。)

接下来我们要用 Rake(参见旁注 2.1)来迁移(migrate)数据库:

$ bundle exec rake db:migrate
==  CreateUsers: migrating ====================================================
-- create_table(:users)
   -> 0.0017s
==  CreateUsers: migrated (0.0018s) ===========================================

上面的命令会使用新定义的 User 数据模型更新数据库。(在 6.1.1 节中将详细介绍数据库迁移)注意,为了使用 Gemfile 中指定的 Rake 版本,我们通过 bundle exec 来执行 rake。(如果你使用 RVM,如前一章所说,可以不加 bundle exec,但为了命令完整,我会一直都加上。不加 bundle exec 还有其他办法,参见 3.6.1 节。)

然后我们可以使用 rails srails server 的缩略形式)来启动本地服务器:

$ rails s

现在演示程序应该已经可以通过 http://localhost:3000/ 查看了。

旁注 2.1:Rake

在 Unix 中,在将源码编译成可执行程序的过程中,make 组件起了很重要的作用。很多程序员的身体甚至已经对下面的代码产生了条件反射

$ ./configure && make && sudo make install

这行代码在 Unix 中(包括 Linux 和 Mac OS X)会对代码进行编译。

Rake 就是 Ruby 版的 make,用 Ruby 编写的类 make 程序。Rails 灵活的运用了 Rake 的功能,特别是提供了一些用来开发基于数据库的 Web 程序所需的任务。rake db:migrate 是最常用的了,还有很多其他的命令,你可以运行 rake -T db 来查看所有和数据库有关的任务:

$ bundle exec rake -T db

如果要查看所有的 Rake 任务,运行

$ bundle exec rake -T

任务列表看起来有点让人摸不着头脑,不过现在无需担心,你不需要知道所有的(或大多数)命令。学完本教程后你会知道所有重要的命令。

demo blank user index rails3

图 2.4:Users 资源的初始索引页面(/users

2.2.1 浏览用户相关的页面

访问根地址 http://localhost:3000/ 得到的还是如图 1.3 所示的 Rails 程序默认页面,不过使用脚手架生成 Users 资源的时候也生成了很多用来处理用户的页面。例如,列出所有用户的页面地址是 /users,创建新用户的地址是 /users/new。本节的目的就是走马观花的浏览一下这些用户相关的页面。浏览的时候你会发现表格 2.1 很有用,表中显示了页面和 URL 地址之间的对应关系。

我们先来看一下显示所有用户的页面,叫做“index”,如你所想,目前还没有用户存在。(如图 2.4)

如果想创建新用户就要访问“new”页面,如图 2.5 所示。(在本地开发时,地址的前面部分都是 http://localhost:3000,因此在后面的内容中我会省略这一部分)在第七章中我们会将其改造成用户注册页面。

表格 2.1:Users 资源中页面和 URL 的对应关系

URL 动作(Action) 目的
/users index 显示所有用户的页面
/users/1 show 显示 ID 为 1 的用户的页面
/users/new new 创建新用户的页面
/users/1/edit edit 编辑 ID 为 1 的用户的页面
demo new user rails3

图 2.5:创建新用户的页面(/users/new

你可以在表格中填入名字和 Email 地址,然后点击创建用户(Create User)按钮来创建一个用户。然后就会显示这个用户的页面(show),如图 2.6 所示。(页面中的绿色文字是通过 Flash 消息实现的,会在 7.4.2 节中介绍)注意页面的地址是 /users/1,正如你猜想的,这里的 1 就是图 2.2 中的用户 id。在 7.1 节 中会将其打造成用户的资料页面。

如果要修改用户的信息就要访问编辑(edit)页面了(如图 2.7)。修改用户的信息后点击“更新用户(Update User)”按钮就更改了演示程序中该用户的信息(如图 2.8)。(在第 6 章我们会看到,用户的数据存储在后端的数据库中。)我们会在 9.1 节中添加编辑和更新用户的功能。

demo show user rails3

图 2.6:显示某个用户的页面(/users/1

现在我们重新回到创建新用户页面,然后提交表格创建第二个用户。然后访问用户索引(index)页面(如图 2.9)。7.1 节将美化一下这个显示所有用户的页面。

我们已经演示了创建、展示、编辑用户的页面,下面要演示销毁用户页面了(如图 2.10)。点击图 2.10 中的链接会出现一个验证对话框,确认后就会删除第二个用户,索引页面就只会显示一个用户。(如果这个操作没有顺利完成,请确保浏览器启用了 JavaScript 支持。销毁用户时 Rails 是通过 JavaScript 发送请求的。)9.4 节会增强用户的删除功能,只有管理员级别的用户才能删除用户。

2.2.2 MVC 实践

我们已经大概的浏览了 Users 资源,下面我们要用 1.2.6 节中介绍的 MVC 的视角来仔细的看一下其中某些特定的部分。我们会分析在浏览器中做一次点击的内在过程,这里通过访问用户索引页面做演示,来了解一下 MVC。(如图 2.11)

  1. 浏览器向 /users 发起一个请求;
  2. Rails 的路由将 /user 分配到 Users 控制器的 index 动作;
  3. index 动作向 User 模型获取所有的用户(User.all);
  4. User 模型从数据库中将所有的用户读取出来;
  5. User 模型将所有的用户返回给控制器;
  6. 控制器将获得的所有用户数据赋予 @users 变量,然后传递给 index 的视图;
  7. 视图使用内嵌 Ruby 代码的模板渲染成 HTML;
  8. 控制器将生成的 HTML 发送回浏览器。3

首先我们要从浏览器中发起一个请求,你可以直接在浏览器地址栏中敲入地址,也可以点击页面中的链接。(图 2.11 中的第 1 步)接着请求到达 Rails 路由(第 2 步),根据 URL 将其分发到适当的控制器动作(而且还会考量请求的类型,旁注 3.2 中会介绍)。将 Users 资源中相关的 URL 映射到控制器动作的代码如代码 2.2 所示。这些代码会按照表格 2.1 中的对应关系做映射。(:users 是一个 Symbol,4.3.3 节会介绍)

代码 2.2:Rails 的路由设置,包含一条 Users 资源的规则

config/routes.rb

DemoApp::Application.routes.draw do
  resources :users
  .
  .
  .
end
demo edit user rails3

图 2.7:编辑用户的页面(/users/1/edit

2.2.1 节中浏览的页面就对应了 Users 控制器中不同的动作。脚手架生成的控制器代码大致如代码 2.3 所示。注意一下 class UsersController < ApplicationController 的用法,这是 Ruby 中类继承的写法。(2.3.4 节中将简要的介绍一下继承,4.4 节将详细介绍类和继承。)

代码 2.3:用户控制器的代码概要

app/controllers/users_controller.rb

class UsersController < ApplicationController
.
.
.
  def index
    .
    .
    .
  end

  def show
    .
    .
    .
  end

  def new
    .
    .
    .
  end

  def create
    .
    .
    .
  end

  def edit
    .
    .
    .
  end

  def update
    .
    .
    .
  end

  def destroy
    .
    .
    .
  end
end
demo update user rails3

图 2.8:显示“信息已更新”提示的用户页面

或许你发现了动作的数量比我们看过的页面数量要多,indexshownewedit 对应了 2.2.1 节中介绍的页面。不过还有一些其他的动作,createupdatedestroy 等,这些动作一般不会直接渲染页面(不过有时也会),它们只会修改数据库中保存的用户数据。表格 2.2 列出的是控制器的全部动作,这些动作就是 Rails 对 REST 架构(参见旁注 2.2)的实现。REST 是由计算机科学家 Roy Fielding 提出的概念,意思是表现层状态转化(Representational State Transfer)。4 注意表格 2.2 中的内容,有些部分是有重叠的。例如 showupdate 两个动作都映射到 /users/1 这个地址上。二者的区别是它们所用的 HTTP 请求方法不同。3.2.1 节将更详细的介绍 HTTP 请求方法。

旁注 2.2:表现层状态转化(REST)

如果你阅读过一些 Ruby on Rails Web 开发相关的资料,你会看到很多地方都提到了“REST”,它是“表现层状态转化(REpresentational State Transfer)”的简称。REST 是一种架构方式,用来开发分布式、基于网络的系统和程序,例如 WWW 和 Web 应用程序。REST 理论是很抽象的,在 Rails 程序中,REST 意味着大多数的组件(例如用户和微博)会被模型化,变成资源(resource),可以被创建(create)、读取(read)、更新(update)和删除(delete),这些操作会与关系型数据库中的 CRUD 操作HTTP 请求方法POSTGETPATCHDELETE)对应起来。(3.2.1 节,特别是旁注 3.3,将更详细的介绍 HTTP 请求)

作为 Rails 程序开发者,REST 开发方式会帮助你决定编写哪些控制器和动作:你只需简单的将可以创建、读取、更新和删除的资源理清就可以了。对本章的用户和微博来说,这一过程非常明确,因为它们都是很自然的资源形式。在第 11 章中将看到 REST 架构允许我们将一个很棘手的问题(“关注用户”功能)通过一种自然而便捷的方式处理。

demo user index two rails3

图 2.9:显示了第二个用户的用户索引页面(/users

demo destroy user rails3

图 2.10:销毁用户

表格 2.2:代码 2.2 中 Users 资源生成的符合 REST 架构的路由

HTTP 请求 URL 动作 目的
GET /users index 显示所用用户的页面
GET /users/1 show 显示 ID 为 1 的用户页面
GET /users/new new 创建新用户的页面
POST /users create 创建新用户
GET /users/1/edit edit 编辑 ID 为 1 的用户页面
PATCH /users/1 update 更新 ID 为 1 的用户
DELETE /users/1 destroy 删除 ID 为 1 的用户
mvc detailed

图 2.11:Rails 中 MVC 的详细说明图解

为了解释 Users 控制器和 User 模型之间的关系,我们要看一下简化了的 index 动作的代码,如代码 2.4 所示。(脚手架生成的代码很粗糙,所以我们做了简化)

代码 2.4:演示程序中被简化了的用户 index 动作

app/controllers/users_controller.rb

class UsersController < ApplicationController
.
.
.
  def index
    @users = User.all
  end
  .
  .
  .
end

index 动作有一行代码是 @users = User.all(图 2.11 中的第 3 步),它要求 User 模型从数据库中取出所有的用户(第 4 步),然后将结果赋值给 @users 变量(第 5 步)。User 模型的代码参见代码 2.5。代码看似简单,不过它通过继承具备了很多功能(参见 2.3.4 节4.4 节)。简单来说就是通过调用 Rails 中叫做 Active Record 的库,代码 2.5 中的 User.all 就会返回所有的用户。

代码 2.5:演示程序中的 User 模型

app/models/user.rb

class User < ActiveRecord::Base
end

一旦定义了 @users 变量,控制器就会调用视图代码(第 6 步),其代码如代码 2.6 所示。以 @ 开头的变量是“实例变量(instance variable)”,在视图中自动可用。在本例中,index.html.erb 视图的代码会遍历 @users,为每个用户生成一行 HTML。(记住,你现在可能读不懂这些代码,这里只是让你看一下这些代码是什么样子。)

代码 2.6:用户索引页面的视图代码

app/views/users/index.html.erb

<h1>Listing users</h1>

<table>
  <tr>
    <th>Name</th>
    <th>Email</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @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.2.3 上述 Users 资源的缺陷

脚手架生成的 Users 资源相关代码虽然能够让你大致的了解一下 Rails,不过它也有一些缺陷:

  • 没有对数据进行验证(validation)。User 模型会接受空的名字和不合法的 Email 地址而不会报错。
  • 没有用户身份验证机制(authentication)。没有实现登录和退出功能,随意一个用户都可以进行任何的操作。
  • 没有测试。也不是完全没有,脚手架会生成一些基本的测试,不过很粗糙也不灵便,没有对数据进行验证,不包含验证机制的测试,以及其他的需求。
  • 没有布局。没有共用的样式和网站导航。
  • 没有真正的被理解。如果你能读懂脚手架生成的代码就不需要阅读本书了。

2.3 Microposts 资源

我们已经生成也浏览了 User 资源,现在要生成 Microposts 资源了。阅读本节时我推荐你和 2.2 节对比一下,你会看到两个资源在很多方面都是一致的。通过这样重复的生成资源我们可以更好的理解 Rails 中的 REST 架构。在这样的早期阶段看一下 Users 资源和 Microposts 资源的相同之处也是本章的主要目的之一。(后面我们会看到,开发一个比本章的演示程序复杂的程序要付出很多汗水,Microposts 资源在第 10 章才会用到,而我不想这么晚才介绍。)

2.3.1 概览 Microposts 资源

和 Users 资源一样,我们使用 rails generate scaffold 命令生成 Microposts 资源的代码,实现图 2.3 中所示的数据模型:5

$ rails generate scaffold Micropost content:string user_id:integer
      invoke  active_record
      create    db/migrate/20130307005528_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  jbuilder_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
      create        test/helpers/microposts_helper_test.rb
      invoke    jbuilder
       exist      app/views/microposts
      create      app/views/microposts/index.json.jbuilder
      create      app/views/microposts/show.json.jbuilder
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/microposts.js.coffee
      invoke    scss
      create      app/assets/stylesheets/microposts.css.scss
      invoke  scss
   identical    app/assets/stylesheets/scaffolds.css.scss

然后要更新数据库,使用最新的数据模型,我们要执行类似 2.2 节中用到的迁移命令:

$ bundle exec rake db:migrate
==  CreateMicroposts: migrating ===============================================
-- create_table(:microposts)
   -> 0.0023s
==  CreateMicroposts: migrated (0.0026s) ======================================

现在我们就可以使用类似 2.2.1 节中介绍的方法来创建微博了。就像你猜测的,脚手架也会更新 Rails 的路由文件,为 Microposts 资源加入一条规则,如代码 2.7 所示。6 和 Users 资源一样,resources :micropsts 会将微博相关的 URL 地址映射到 Microposts 控制器,如表格 2.3 所示。

表格 2.3:代码 2.7 中 Microposts 资源生成的符合 REST 架构的路由

HTTP 请求 URL 动作 目的
GET /microposts index 显示所有微博的页面
GET /microposts/1 show 显示 ID 为 1 的微博页面
GET /microposts/new new 显示创建新微博的页面
POST /microposts create 创建新微博
GET /microposts/1/edit edit 编辑 ID 为 1 的微博页面
PATCH /microposts/1 update 更新 ID 为 1 的微博
DELETE /microposts/1 destroy 删除 ID 为 1 的微博

代码 2.7:Rails 的路由配置,有一条针对 Microposts 资源的新规则

config/routes.rb

DemoApp::Application.routes.draw do
  resources :microposts
  resources :users
  .
  .
  .
end

Microposts 控制器的代码简化后如代码 2.8 所示。注意,除了将 UsersController 换成 MicropostsController 之外,这段代码和代码 2.3 没什么区别。这说明了这两个资源在 REST 架构中的共同之处。

代码 2.8:Microposts 控制器的代码简化形式

app/controllers/microposts_controller.rb

class MicropostsController < ApplicationController
.
.
.
  def index
    .
    .
    .
  end

  def show
    .
    .
    .
  end

  def new
    .
    .
    .
  end

  def create
    .
    .
    .
  end

  def edit
    .
    .
    .
  end

  def update
    .
    .
    .
  end

  def destroy
    .
    .
    .
  end
end

我们在创建微博页面(/microposts/new)输入一些内容来添加一个微博,如图 2.12 所示。

demo new micropost rails3

图 2.12:创建微博的页面(/microposts/new

既然已经在这个页面了,那就多创建几个微博,确保至少有一个微博的 user_id 设为了 1,这样就对应到 2.2.1 节中创建的第一个用户了。结果应该和图 2.13 类似。

2.3.2 限制微博内容的长度

如果要称得上微博这样的名字就要限制其内容的长度。在 Rails 中实现这种限制很简单,使用数据验证(validation)功能。要限制微博的长度最大为 140 个字符(就像 Twitter 一样),我们可以使用长度限制数据验证。现在你可以用你的文本编辑器或 IDE 打开 app/models/micropost.rb 写入代码 2.9 所示的代码。(代码 2.9 中使用的 validates 方法只针对 Rails 3;如果你之前用过 Rails 2.3,就可以对比一下它和 validates_length_of 的区别。)

demo micropost index rails3

图 2.13:微博索引页面(/microposts

代码 2.9:现在微博的长度最长为 140 个字符

app/models/micropost.rb

class Micropost < ActiveRecord::Base
  validates :content, length: { maximum: 140 }
end

上面的代码看起来可能很神秘,我们会在 6.2 节中详细介绍数据验证。如果我们在创建微博页面输入超过 140 个字符的内容就会看到这个验证的样子了。如图 2.14 所示,Rails 会显示一个错误提示信息(error message)提示微博的内容太长了。(7.3.3 节将更详细的介绍错误信息)

2.3.3 一个用户有多篇微博

Rails 强大的功能之一是可以为不同的数据模型之间创建关联(association)。针对本例中的 User 模型,每个用户可以有多篇微博。我们可以通过更新 User 模型(参见代码 2.10)和 Micropost 模型(参见代码 2.11)的代码来实现这种关联。

micropost length error rails3

图 2.14:创建微博失败后显示的错误信息

代码 2.10:一个用户有多篇微博

app/models/user.rb

class User < ActiveRecord::Base
  has_many :microposts
end

代码 2.11:一篇微博只属于一个用户

app/models/micropost.rb

class Micropost < ActiveRecord::Base
  belongs_to :user
  validates :content, length: { maximum: 140 }
end

我们可以将这种关联用图 2.15 所示的图形表示出来。因为 microposts 表中有 user_id 这一列,所以 Rails(通过 Active Record)就可以将微博和每个用户关联起来。

micropost user association

图 2.15:微博和用户之间的关联

第 10 章第 11 章中,我们将使用用户和微博之间的关联来显示某一个用户的所有微博,并且生成一个和 Twitter 类似的动态列表。我们可以使用控制台(console)来检查一下用户与微博之间关联的实现,控制台是和 Rails 应用程序交互很有用的工具。在命令行中执行 rails console 来启动控制台,然后使用 User.first 从数据库中读取第一个用户(并将读取的数据赋值给 first_user 变量):7

$ rails console
>> first_user = User.first
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2013-03-06 02:01:31", updated_at: "2013-03-06 02:01:31">
>> first_user.microposts
=> [#<Micropost id: 1, content: "First micropost!", user_id: 1, created_at:
"2013-03-06 02:37:37", updated_at: "2013-03-06 02:37:37">, #<Micropost id: 2,
content: "Second micropost", user_id: 1, created_at: "2013-03-06 02:38:54",
updated_at: "2013-03-06 02:38:54">]
>> exit

(上面代码中我包含了最后一行用来演示如何退出控制台,在大多数系统中也可以使用 Ctrl-d 组合键。)然后使用 first_user.microposts 获取用户的微博:Active Record 会自动返回 user_idfirst_user 的 id 相同的(1)所有微博。我们将在第 10 章第 11 章更详细的学习 Active Record 中这种关联的实现。

2.3.4 继承关系

接下来我们暂时结束演示程序的讨论,来简单的介绍一下 Rails 中控制器和模型的类继承。如果你有一些面向对象编程(Object-oriented Programming,OOP)的经验将更好的理解这些内容,如果你未接触过 OOP的话可以选择跳过本小节。一般来说,如果你不熟悉类的概念(4.4 节中会介绍),我建议你稍晚些时候再回过头来看本小节。

我们先介绍模型的继承关系。对比一下代码 2.12 和代码 2.13 中的代码,User 模型和 Micropost 模型都继承自(通过 <ActiveRecord::Base,它是 ActiveRecord 为模型提供的基类。图 2.16 列出了这种继承关系。通过继承 ActiveRecord::Base 我们的模型对象才能够和数据库通讯、将数据库中的列看做 Ruby 中的属性等。

代码 2.12:User 类,包括继承关系

app/models/user.rb

class User < ActiveRecord::Base
  .
  .
  .
end

代码 2.13:Micropost 类,包括继承关系

app/models/micropost.rb

class Micropost < ActiveRecord::Base
  .
  .
  .
end
demo model inheritance

图 2.16:User 模型和 Micropost 模型的继承关系

控制器的继承关系更复杂一些。对比一下代码 2.14 和代码 2.15,我们可以看到 Users 控制器和 Microposts 控制器都继承自应用程序的控制器(ApplicationController)。如代码 2.16 所示,ApplicationController 继承自 ActionController::Base,它是 Rails 中的 Action Pack 库为控制器提供的基类。这些类之间的关系如图 2.17 所示。

demo controller inheritance

图 2.17:Users 控制器和 Microposts 控制器的继承关系

代码 2.14:UsersController 类,包含继承关系

app/controllers/users_controller.rb

class UsersController < ApplicationController
  .
  .
  .
end

代码 2.15:MicropostsController 类,包含继承关系

app/controllers/microposts_controller.rb

class MicropostsController < ApplicationController
  .
  .
  .
end

代码 2.16:ApplicationController 类,包含继承关系

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  .
  .
  .
end

和模型的继承类似,通过继承 ActionController::Base,Users 控制器和 Microposts 控制器获得了很多的功能,例如处理模型对象的功能,过滤输入的 HTTP 请求,以及将视图渲染成 HTML 的功能。因为 Rails 中的控制器都继承自 ApplicationController,所以在应用程序控制器中定义的内容就会应用到程序中的所有动作。例如,在 8.2.1 节中将看到如何在应用程序控制器中添加一个登录、退出的帮助方法。

2.3.5 部署演示程序

完成 Microposts 资源之后,是时候将代码推送到 GitHub 的仓库中了:

$ git add .
$ git commit -m "Finish demo app"
$ git push

通常情况下,你应该经常做一些很小的提交,不过对于本章来说最后做一次大的提交也可以。

然后,你也可以按照 1.4 节中介绍的方法将演示程序部署到 Heroku:

$ heroku create
$ rake assets:precompile
$ git add .
$ git commit -m "Add precompiled assets for Heroku"
$ git push heroku master

最后,迁移生产环境中的数据库:

$ heroku run rake db:migrate

上面的代码会用 User 和 Micropost 数据模型更新 Heroku 上的数据库。

2.4 小结

现在我们已经结束了对一个 Rails 应用程序的分析,本章中开发的演示程序有一些好的地方也有一些有缺陷的地方。

好的地方

  • 概览了 Rails
  • 介绍了 MVC
  • 第一次体验了 REST 架构
  • 开始使用数据模型了
  • 在生产环境中运行了一个基于数据库的 Web 程序

有缺陷的地方

  • 没有自定义布局和样式
  • 没有静态页面(例如“首页”和“关于”)
  • 没有用户密码
  • 没有用户头像
  • 没登录功能
  • 不安全
  • 没实现用户和微博的自动关联
  • 没实现“关注”和“被关注”功能
  • 没实现动态列表
  • 没使用 TDD
  • 没有真的理解所做的事情

本书后续的内容会建立在这些好的部分之上,然后改善有缺陷的部分。

  1. 如果要实现内容更长的文章,例如一篇常规博客中的文章,应该将字符串类型(string)换成文本类型(text);

  2. 脚手架后面跟着的名字和模型一样,是单数形式,而资源和控制器是复数形式。因此是 User 而不是 Users

  3. 有些文章会说是视图直接将 HTML 返回给浏览器的(通过 Web 服务器,例如 Apache 和 Nginx)。不管实现的细节是怎样的,我更相信控制器是一个中枢,应用程序中所有的信息都会通过它;

  4. 加利福尼亚大学欧文分校 2000 年 Roy Thomas Fielding 的博士论文《架构风格与基于网络的软件架构设计》(译者注:中文翻译

  5. 和生成 Users 资源的脚手架命令一样,生成 Microposts 资源的脚手架也使用了单数形式,因此我们使用 generate Micropost

  6. 和代码 2.7 相比,脚手架生成的代码可能会有额外的空行。你无须担心,因为 Ruby 会忽略额外的空行;

  7. 你的控制台可能会显示类似 ruby-2.0.0-head > 的开头,示例中使用 >> 替代,因为不同的 Ruby 版本会有所不同。