6.1 用户模型
最后更新于:2022-04-01 22:29:00
# 6.1 用户模型
接下来的三章要实现网站的“注册”页面(构思图如[图 6.1](#fig-signup-mockup-preview) 所示),在此之前我们先要解决存储问题,因为现在还没地方存储用户信息。所以,实现用户注册功能的第一步是,创建一个数据结构,获取并存储用户的信息。
![signup mockup bootstrap](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bd08d8ea7.png)图 6.1:用户注册页面的构思图
在 Rails 中,数据模型的默认数据结构叫“模型”(model,MVC 中的 M,参见 [1.3.3 节](chapter1.html#model-view-controller))。Rails 为解决数据持久化提供的默认解决方案是,使用数据库存储需要长期使用的数据。和数据库交互默认使用的是 Active Record。[[1](#fn-1)]Active Record 提供了一系列方法,无需使用[关系数据库](http://en.wikipedia.org/wiki/Relational_database)所用的“结构化查询语言”(Structured Query Language,简称 SQL),[[2](#fn-2)]就能创建、保存和查询数据对象。Rails 还支持“迁移”(migration)功能,允许我们使用纯 Ruby 代码定义数据结构,而不用学习 SQL “数据定义语言”(Data Definition Language,简称 DDL)。最终的结果是,Active Record 把你和数据存储层完全隔开了。本书开发的应用在本地使用 SQLite,部署后使用 PostgreSQL(由 Heroku 提供,参见 [1.5 节](chapter1.html#deploying))。这就引出了一个更深层的话题——在不同的环境中,即便使用不同类型的数据库,我们也无需关心 Rails 是如何存储数据的。
和之前一样,如果使用 Git 做版本控制,现在应该新建一个主题分支:
```
$ git checkout master
$ git checkout -b modeling-users
```
## 6.1.1 数据库迁移
回顾一下 [4.4.5 节](chapter4.html#a-user-class)的内容,在我们自己创建的 `User` 类中为用户对象定义了 `name` 和 `email` 两个属性。那是个很有用的例子,但没有实现持久性最关键的要求:在 Rails 控制台中创建的用户对象,退出控制台后就会消失。本节的目的是为用户创建一个模型,让用户数据不会这么轻易消失。
和 [4.4.5 节](chapter4.html#a-user-class)中定义的 `User` 类一样,我们先为用户模型创建两个属性,分别是 `name` 和 `email`。我们会把 `email` 属性用作唯一的用户名。[[3](#fn-3)]([6.3 节](#adding-a-secure-password)会添加一个属性,存储密码)在[代码清单 4.13](chapter4.html#listing-example-user) 中,我们使用 Ruby 的 `attr_accessor` 方法创建了这两个属性:
```
class User
attr_accessor :name, :email
.
.
.
end
```
不过,在 Rails 中不用这样定义属性。前面提到过,Rails 默认使用关系数据库存储数据,数据库中的表由数据行组成,每一行都有相应的列,对应数据属性。例如,为了存储用户的名字和电子邮件地址,我们要创建 `users` 表,表中有两个列,`name` 和 `email`,这样每一行就表示一个用户,如[图 6.2](#fig-users-table) 所示,对应的数据模型如[图 6.3](#fig-user-model-sketch) 所示。(图 6.3 只是梗概,完整的数据模型如[图 6.4](#fig-user-model-initial) 所示。)把列命名为 `name` 和 `email` 后,Active Record 会自动把它们识别为用户对象的属性。
![users table](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bd08f0351.png)图 6.2:`users` 表中的示例数据![user model sketch](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bd090ff29.png)图 6.3:用户数据模型梗概
你可能还记得,在[代码清单 5.28](chapter5.html#listing-generate-users-controller) 中,我们使用下面的命令生成了用户控制器和 `new` 动作:
```
$ rails generate controller Users new
```
创建模型有个类似的命令——`generate model`。我们可以使用这个命令生成用户模型,以及 `name` 和 `email` 属性,如[代码清单 6.1](#listing-generate-user-model) 所示。
##### 代码清单 6.1:生成用户模型
```
$ rails generate model User name:string email:string
invoke active_record
create db/migrate/20140724010738_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
```
(注意,控制器名是复数,模型名是单数:控制器是 `Users`,而模型是 `User`。)我们指定了可选的参数 `name:string` 和 `email:string`,告诉 Rails 我们需要的两个属性是什么,以及各自的类型(两个都是字符串)。你可以把这两个参数与[代码清单 3.4](chapter3.html#listing-generating-pages) 和[代码清单 5.28](chapter5.html#listing-generate-users-controller) 中的动作名对比一下,看看有什么不同。
执行上述 `generate` 命令之后,会生成一个迁移文件。迁移是一种递进修改数据库结构的方式,可以根据需求修改数据模型。执行 `generate` 命令后会自动为用户模型创建迁移,这个迁移的作用是创建一个 `users` 表以及 `name` 和 `email` 两个列,如[代码清单 6.2](#listing-users-migration) 所示。(我们会在 [6.2.5 节](#uniqueness-validation)介绍如何手动创建迁移文件。)
##### 代码清单 6.2:用户模型的迁移文件(创建 `users` 表)
db/migrate/[timestamp]_create_users.rb
```
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps null: false
end
end
end
```
注意,迁移文件名前面有个时间戳,指明创建的时间。早期,迁移文件名的前缀是递增的数字,在团队协作中,如果多个程序员生成了序号相同的迁移文件就可能会发生冲突。除非两个迁移文件在同一秒钟生成这种小概率事件发生了,否则使用时间戳基本可以避免冲突的发生。
迁移文件中有一个名为 `change` 的方法,定义要对数据库做什么操作。在[代码清单 6.2](#listing-users-migration) 中,`change` 方法使用 Rails 提供的 `create_table` 方法在数据库中新建一个表,用来存储用户。`create_table` 方法可以接受一个块,块中有一个块变量 `t`(“table”)。在块中,`create_table` 方法通过 `t` 对象创建 `name` 和 `email` 两个列,均为 `string` 类型。[[4](#fn-4)]表名是复数形式(`users`),不过模型名是单数形式(`User`),这是 Rails 在用词上的一个约定:模型表示单个用户,而数据库表中存储了很多用户。块中最后一行 `t.timestamps null: false` 是个特殊的方法,它会自动创建两个列,`created_at` 和 `updated_at`,这两个列分别记录创建用户的时间戳和更新用户数据的时间戳。([6.1.3 节](#creating-user-objects)有使用这两个列的例子。)这个迁移文件表示的完整数据模型如[图 6.4](#fig-user-model-initial) 所示。(注意,[图 6.3](#fig-user-model-sketch) 中没有列出自动添加的两个时间戳列。)
![user model initial 3rd edition](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bd091f455.png)图 6.4:[代码清单 6.2](#listing-users-migration) 生成的用户数据模型
我们可以使用如下的 `rake` 命令([旁注 2.1](chapter2.html#aside-rake))执行这个迁移(叫“向上迁移”):
```
$ bundle exec rake db:migrate
```
(你可能还记得,我们在 [2.2 节](chapter2.html#the-users-resource)用过这个命令。)第一次运行 `db:migrate` 命令时会创建 `db/development.sqlite3`,这是 SQLite [[5](#fn-5)]数据库文件。若要查看数据库结构,可以使用 [SQLite 数据库浏览器](http://sqlitebrowser.org/)打开 `db/development.sqlite3` 文件,如[图 6.5](#fig-sqlite-database-browser) 所示。(如果想从云端 IDE 把这个文件下载到本地电脑,可以在 `db/development.sqlite3` 上按右键,然后选择“Download”。)和[图 6.4](#fig-user-model-initial) 中的模型对比之后,你可能会发现有一个列在迁移中没有出现——`id` 列。[2.2 节](chapter2.html#the-users-resource)提到过,这个列是自动生成的,Rails 用这个列作为行的唯一标识符。
![sqlite database browser 3rd edition](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bd0931aa3.png)图 6.5:在 SQLite 数据库浏览器中查看刚创建的 `users` 表
大多数迁移,包括本书中的所有迁移,都是可逆的,也就是说可以使用一个简单的 Rake 命令“向下迁移”,撤销之前的操作,这个命令是 `db:rollback`:
```
$ bundle exec rake db:rollback
```
(还有一个撤销迁移的方法,参见[旁注 3.1](chapter3.html#aside-undoing-things)。)这个命令会调用 `drop_table` 方法,把 `users` 表从数据库中删除。之所以可以这么做,是因为 `change` 方法知道 `create_table` 的逆操作是 `drop_table`,所以回滚时会直接调用 `drop_table` 方法。对于一些无法自动逆转的操作,例如删除列,就不能依赖 `change` 方法了,我们要分别定义 `up` 和 `down` 方法。关于迁移的更多信息请查看 [Rails 指南](http://guides.rubyonrails.org/migrations.html)。
如果你执行了上面的回滚操作,在继续阅读之前请再迁移回来:
```
$ bundle exec rake db:migrate
```
## 6.1.2 模型文件
我们看到,执行[代码清单 6.1](#listing-generate-user-model) 中的命令后会生成一个迁移文件([代码清单 6.2](#listing-users-migration)),也看到了执行迁移后得到的结果([图 6.5](#fig-sqlite-database-browser)):修改 `db/development.sqlite3` 文件,新建 `users` 表,并创建 `id`、`name`、`email`、`created_at` 和 `updated_at` 这几个列。[代码清单 6.1](#listing-generate-user-model) 同时还生成了一个模型文件,本节剩下的内容专门解说这个文件。
我们先看用户模型的代码,在 `app/models/` 文件夹中的 `user.rb` 文件里。这个文件的内容非常简单,如[代码清单 6.3](#listing-raw-user-model) 所示。
##### 代码清单 6.3:刚创建的用户模型
app/models/user.rb
```
class User < ActiveRecord::Base
end
```
[4.4.2 节](chapter4.html#class-inheritance)介绍过,`class User < ActiveRecord::Base` 的意思是 `User` 类继承自 `ActiveRecord::Base` 类,所以用户模型自动获得了 `ActiveRecord::Base` 的所有功能。当然了,只知道这种继承关系没什么用,我们并不知道 `ActiveRecord::Base` 做了什么。下面看几个实例。
## 6.1.3 创建用户对象
和[第 4 章](chapter4.html#rails-flavored-ruby)一样,探索数据模型使用的工具是 Rails 控制台。因为我们还不想修改数据库中的数据,所以要在沙盒模式中启动控制台:
```
$ rails console --sandbox
Loading development environment in sandbox
Any modifications you make will be rolled back on exit
>>
```
如提示消息所说,“Any modifications you make will be rolled back on exit”,在沙盒模式下使用控制台,退出当前会话后,对数据库做的所有改动都会回归到原来的状态。
在 [4.4.5 节](chapter4.html#a-user-class)的控制台会话中,我们要引入[代码清单 4.13](chapter4.html#listing-example-user) 中的代码才能使用 `User.new` 创建用户对象。对模型来说,情况有所不同。你可能还记得 [4.4.4 节](chapter4.html#a-controller-class)说过,Rails 控制台会自动加载 Rails 环境,这其中就包括模型。也就是说,现在无需加载任何代码就可以直接创建用户对象:
```
>> User.new
=> #
```
上述代码显示了一个用户对象的默认值。
如果不为 `User.new` 指定参数,对象的所有属性值都是 `nil`。在 [4.4.5 节](chapter4.html#a-user-class),自己编写的 `User` 类可以接受一个哈希参数初始化对象的属性。这种方式是受 Active Record 启发的,在 Active Record 中也可以使用相同的方式指定初始值:
```
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #
```
我们看到 `name` 和 `email` 属性的值都已经设定了。
数据的有效性对理解 Active Record 模型对象很重要,我们会在 [6.2 节](#user-validations)深入介绍。不过注意,现在这个 `user` 对象是有效的,我们可以在这个对象上调用 `valid?` 方法确认:
```
>> user.valid?
true
```
到目前为止,我们都没有修改数据库:`User.new` 只在内存中创建一个对象,`user.valid?` 只是检查对象是否有效。如果想把用户对象保存到数据库中,我们要在 `user` 变量上调用 `save` 方法:
```
>> user.save
(0.2ms) begin transaction
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users".
"email") = LOWER('mhartl@example.com') LIMIT 1
SQL (0.5ms) INSERT INTO "users" ("created_at", "email", "name", "updated_at)
VALUES (?, ?, ?, ?) [["created_at", "2014-09-11 14:32:14.199519"],
["email", "mhartl@example.com"], ["name", "Michael Hartl"], ["updated_at",
"2014-09-11 14:32:14.199519"]]
(0.9ms) commit transaction
=> true
```
如果保存成功,`save` 方法返回 `true`,否则返回 `false`。(现在所有保存操作都会成功,因为还没有数据验证功能,[6.2 节](#user-validations)会看到一些失败的例子。)Rails 还会在控制台中显示 `user.save` 对应的 SQL 语句(`INSERT INTO "users"…`),以供参考。本书几乎不会使用原始的 SQL,[[6](#fn-6)]所以此后会省略 SQL。不过,从 Active Record 各种操作生成的 SQL 中可以学到很多知识。
你可能注意到了,刚创建时用户对象的 `id`、`created_at` 和 `updated_at` 属性值都是 `nil`,下面看一下保存之后有没有变化:
```
>> user
=> #
```
我们看到,`id` 的值变成了 `1`,那两个自动创建的时间戳属性也变成了当前时间。[[7](#fn-7)]现在这两个时间戳是一样的,[6.1.5 节](#updating-user-objects)会看到二者不同的情况。
和 [4.4.5 节](chapter4.html#a-user-class)的 `User` 类一样,用户模型的实例也可以使用点号获取属性:
```
>> user.name
=> "Michael Hartl"
>> user.email
=> "mhartl@example.com"
>> user.updated_at
=> Thu, 24 Jul 2014 00:57:46 UTC +00:00
```
[第 7 章](chapter7.html#sign-up)会介绍,虽然一般习惯把创建和保存分成如上所示的两步完成,不过 Active Record 也允许我们使用 `User.create` 方法把这两步合成一步:
```
>> User.create(name: "A Nother", email: "another@example.org")
#
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#
```
注意,`User.create` 的返回值不是 `true` 或 `false`,而是创建的用户对象,可直接赋值给变量(例如上面第二个命令中的 `foo` 变量).
`create` 的逆操作是 `destroy`:
```
>> foo.destroy
=> #
```
奇怪的是,`destroy` 和 `create` 一样,返回值是对象。我不觉得什么地方会用到 `destroy` 的返回值。更奇怪的是,销毁的对象还在内存中:
```
>> foo
=> #
```
那么我们怎么知道对象是否真被销毁了呢?对于已经保存而没有销毁的对象,怎样从数据库中读取呢?要回答这些问题,我们要先学习如何使用 Active Record 查找用户对象。
## 6.1.4 查找用户对象
Active Record 提供了好几种查找对象的方法。下面我们使用这些方法查找创建的第一个用户,同时也验证一下第三个用户(`foo`)是否被销毁了。先看一下还存在的用户:
```
>> User.find(1)
=> #
```
我们把用户的 ID 传给 `User.find` 方法,Active Record 会返回 ID 为 1 的用户对象。
下面来看一下 ID 为 3 的用户是否还在数据库中:
```
>> User.find(3)
ActiveRecord::RecordNotFound: Couldn't find User with ID=3
```
因为我们在 [6.1.3 节](#creating-user-objects)销毁了第三个用户,所以 Active Record 无法在数据库中找到这个用户,抛出了一个异常,这说明在查找过程中出现了问题。因为 ID 不存在,所以 `find` 方法抛出 `ActiveRecord::RecordNotFound` 异常。[[8](#fn-8)]
除了这种查找方法之外,Active Record 还支持通过属性查找用户:
```
>> User.find_by(email: "mhartl@example.com")
=> #
```
我们会使用电子邮件地址做用户名,所以在学习如何让用户登录网站时会用到这种 `find` 方法([第 7 章](chapter7.html#sign-up))。你可能会担心如果用户数量过多,使用 `find_by` 的效率不高。事实的确如此,我们会在 [6.2.5 节](#uniqueness-validation)说明这个问题,以及如何使用数据库索引解决。
最后,再介绍几个常用的查找方法。首先是 `first` 方法:
```
>> User.first
=> #
```
很明显,`first` 会返回数据库中的第一个用户。还有 `all` 方法:
```
>> User.all
=> #, #]>
```
从控制台的输出可以看出,`User.all` 方法返回一个 `ActiveRecord::Relation` 实例,其实这是一个数组([4.3.1 节](chapter4.html#arrays-and-ranges)), 包含数据库中的所有用户。
## 6.1.5 更新用户对象
创建对象后,一般都会进行更新操作。更新有两种基本方式,其一,可以分别为各属性赋值,在 [4.4.5 节](chapter4.html#a-user-class)就是这么做的:
```
>> user # 只是为了查看 user 对象的属性是什么
=> #
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true
```
注意,如果想把改动写入数据库,必须执行最后一个方法。我们可以执行 `reload` 命令来看一下没保存的话是什么情况。`reload` 命令会使用数据库中的数据重新加载对象:
```
>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
=> "mhartl@example.net"
```
现在我们已经更新了用户数据,如在 [6.1.3 节](#creating-user-objects)中所说,自动创建的那两个时间戳属性不一样了:
```
>> user.created_at
=> "2014-07-24 00:57:46"
>> user.updated_at
=> "2014-07-24 01:37:32"
```
更新数据的第二种常用方式是使用 `update_attributes` 方法:[[9](#fn-9)]
```
>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"
```
`update_attributes` 方法接受一个指定对象属性的哈希作为参数,如果操作成功,会执行更新和保存两个操作(保存成功时返回值为 `true`)。注意,如果任何一个数据验证失败了,例如存储记录时需要密码([6.3 节](#adding-a-secure-password)实现),`update_attributes` 操作就会失败。如果只需要更新单个属性,可以使用 `update_attribute`,跳过验证:
```
>> user.update_attribute(:name, "The Dude")
=> true
>> user.name
=> "The Dude"
```
';