用户建模
一. User 模型
实现用户注册功能的第一步是,创建一个数据结构,用于存取用户的信息。
在 Rails 中,数据模型的默认数据结构叫模型(model,MVC 中的 M)。Rails 为解决数据持久化提供的默认解决方案是,使用数据库存储需要长期使用的数据。与数据库交互默认使用的是 ActiveRecord。Active Record 提供了一系列方法,无需使用关系数据库所用的结构化查询语言(Structured QueryLanguage,简称 sql),就能创建、保存和查询数据对象。Rails 还支持迁移(migration)功能,允许我们使用纯 Ruby 代码定义数据结构,而不用学习 sql 数据定义语言(Data DeFinition Language,简称 DDL)。最终的结果是,Active Record 把你和数据库完全隔开了。咱们开发的应用在本地使用 sqlite,部署后使用Postgresql。这就引出了一个更深层的话题——在不同的环境中,即便使用不同类型的数据库,我们也无需关心 Rails 是如何存储数据的。
1.数据库迁移
回顾一下前面的内容, 我们在自己创建的 User 类中为用户对象定义了 name 和 email 两个属性。那是个很有用的例子, 但没有实现持久化存储最关键的要求: 在 Rails 控制台中创建的用户对象, 退出控制台后就会消失。这次的目的是为用户创建一个模型, 让用户数据不会这么轻易消失。
与前面定义的 User 类一样, 我们先为 User 模型创建两个属性, 分别为 name 和 email 。我们会把 email 属性用作唯一的用户名。 (下面也会添加一个属性, 用于存储密码。)在前面的代码中,我们使用 Ruby的 attr_accessor 方法创建了这两个属性:
class User attr_accessor :name, :email ... end
不过, 在 Rails 中不用这样定义属性。前面提到过,Rails 默认使用关系数据库存储数据, 数据库中的表由数据行(row)组成, 每一行都有相应的列(column), 对应于数据属性。例如, 为了存储用户的名字和电子邮件地址, 我们要创建 users 表, 表中有两个列, name 和 email , 这样每一行就表示一个用户, 如下图所
示, 对应的数据模型如下图所示。(下图只是梗概, 完整的数据模型请往下看。)把列命名为 name 和 email 后, Active Record 会自动把它们识别为用户对象的属性。
注:你可能还记得, 在上面的代码中, 我们使用下面的命令生成了 Users 控制器和 new 动作$ rails generate controller Users new
创建模型有个类似的命令 —— generate model 。我们可以使用这个命令生成 User 模型, 以及 name 和 email 属性, 如下代码所示。
(1).生成 User 模型
注:控制器名是复数, 模型名是单数: 控制器是 Users , 而模型是 User 。我们指定了可选的参数 name:string 和 email:string , 告诉 Rails 我们需要的两个属性是什么, 以及各自的类型(两个都是字符串)。
执行上述 generate 命令之后, 会生成一个迁移文件。迁移是一种递进修改数据库结构的方式, 可以根据需求修改数据模型。执行上述 generate 命令后会自动为 User 模型创建迁移, 这个迁移的作用是创建一个 users 表, 以及 name 和 email 两个列, 如下代码所示:
(2).User 模型的迁移文件(创建 users 表)
打开文件:db/migrate/[timestamp]_create_users.rb
注:迁移文件名前面有个时间戳(timestamp),指明创建的时间。早期, 迁移文件名的前缀是递增的数字, 在团队协作中, 如果多人生成了序号相同的迁移文件就可能会发生冲突。除非两个迁移文件在同一秒钟生成这种小概率事件发生了, 否则使用时间戳基本可以避免冲突。
迁移文件中有一个名为 change 的方法,定义要对数据库做什么操作。在上图代码中, change 方法使用 Rails 提供的 create_table 方法在数据库中新建一个表,用于存储用户。create_table 方法可以接受一个块,有一个块变量 t (“table”)。在块中, create_table 方法通过 t 对象在数据库中创建 name 和 email 两个列,二者均为 string 类型。表名是复数形式( users ),不过模型名是单数形式( User ),这是 Rails 在用词上的一个约定:模型表示单个用户,而数据库表中存储了很多用户。块中最后一行 t.timestamps 是个特殊的方法,它会自动创建 created_at 和 updated_at 两个列,分别记录创建用户的时间戳和更新用户的时间戳。(前面有使用这两个列的例子。)这个迁移文件表示的完整数据模型如下图所示。
我们可以使用如下的 db:migrate 命令执行这个迁移(这叫“向上迁移”):
$ rails db:migrate
注:大多数迁移, 都是可逆的, 也就是说可以使用一个简单的命令“向下迁移”, 撤销之前的操作。这个命令是 db:rollback :
$ rails db:rollback
2.模型文件
(1).刚创建的 User 模型
打开文件:app/models/user.rb
注:前面说过, class User < ApplicationRecord 的意思是 User 类继承自 ApplicationRecord 类(而它继承自 ActiveRecord::Base 类), 所以 User 模型自动获得了 ActiveRecord::Base 的所有功能。
(2). 创建用户对象
探索数据模型使用的工具是 Rails 控制台。因为我们(还)不想修改数据库中的数据, 所以要在沙盒(sandBox)模式中启动控制台:
$ rails console --sandBox
注:如提示消息所说, “Any modifications you make will be rolled back on exit”, 在沙盒模式下使用控制台, 退出当前会话后, 对数据库做的所有改动都会回归到原来的状态(即撤销)。
在前面的控制台会话中, 我们要引入必要的代码才能使用 User.new 创建用户对象。对模型来说, 情况有所不同。你可能还记得前面说过, Rails 控制台会自动加载 Rails 环境, 这其中就包括模型。也就是说, 现在无需加载任何代码就可以直接创建用户对象:
如果不为 User.new 指定参数, 对象的所有属性值都是 nil 。前面, 我们自己编写的 User 类可以接受一个散列参数, 指定用于初始化对象的属性。这种方式是受 Active Record 启发的, 在 Active Record 中也可以使用相同的方式指定初始值:
注:我们看到 name 和 email 属性的值都已经按预期设定了。
数据的有效性(validity)对理解 Active Record 模型对象很重要, 我们会在以后深入探讨。不过注意, 现在这个 user 对象是有效的, 我们可以在这个对象上调用 valid? 方法确认:
目前为止, 我们都没有修改数据库: User.new 只在内存中创建一个对象, user.valid? 只是检查对象是否有效。如果想把用户对象保存到数据库中, 要在 user 变量上调用 save 方法:
注:如果保存成功, save 方法返回 true , 否则返回 false 。(现在所有保存操作都会成功, 因为还没有数据验证; 等到了下面的内容就会看到一些失败的例子。) Rails 还会在控制台中显示 user.save 对应的 sql 语句( INSERT INTO "users"... ), 以供参考。我们几乎不会使用原始的 sql, 所以此后我会省略 sql。不过, 从 Active Record各种操作生成的 sql 中可以学到很多东西。
与以前定义的 User 类一样, User 模型的实例也可以使用点号获取属性:
等以后会介绍, 虽然一般习惯把创建和保存分成如上所示的两步完成, 不过 Active Record 也允许我们使用User.create 方法把这两步合成一步:
注:User.create 的返回值不是 true 或 false , 而是创建的用户对象, 可以直接赋值给变量(例如上面第二个命令中的 foo 变量)
create 的逆操作是 destroy :
奇怪的是, destroy 和 create 一样, 返回值是对象。我不觉得什么地方会用到 destroy 的返回值。更奇怪的是, 销毁的对象还在内存中:
注:那么我们怎么知道对象是否真被销毁了呢? 对于已经保存而没有销毁的对象, 怎样从数据库中读取呢? 要回答这些问题, 我们要先学习如何使用 Active Record 查找用户对象。
(3).查找用户对象
Active Record 提供了好几种查找对象的方法。下面我们使用这些方法查找前面创建的第一个用户, 同时也验证一下第三个用户( foo )是否被销毁了。先看一下还存在的用户:
注:我们把用户的 ID 传给 User.find 方法, Active Record 会返回 ID 为 1 的用户对象。
注:因为我们在前面销毁了第三个用户, 所以 Active Record 无法在数据库中找到这个用户, 从而抛出一个异常(exception), 这说明在查找过程中出现了问题。因为 ID 不存在, 所以 find 方法抛出 ActiveRe-cord::RecordNotFound 异常。
除了这种查找方式之外, Active Record 还支持通过属性查找用户:
注:我们将使用电子邮件地址做用户名, 在学习如何让用户登录网站时会用到这种 find 方法(后面的内容)。你可能会担心如果用户数量过多, 使用 find_by 的效率不高。事实的确如此, 我们会在下面说明这个问题, 以及如何使用数据库索引解决。
很明显, first 会返回数据库中的第一个用户。还有 all 方法:
从控制台的输出可以看出, User.all 方法返回一个 ActiveRecord::Relation 实例, 其实这是一个数组, 包含数据库中的所有用户。