Ruby Rails学习中:User 模型,验证用户数据

摘要:
用户建模I用户模型实现用户注册功能的第一步是创建用于访问用户信息的数据结构。在Rails中,数据模型的默认数据结构称为模型。Rails为数据持久性提供的默认解决方案是使用数据库存储需要长期使用的数据。)列命名为name和email后,ActiveRecord将自动将它们识别为用户对象的属性。表名是复数形式,但模型名是单数形式。这是Rails的惯例:模型表示单个用户,而数据库表存储许多用户。
用户建模 一. 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 会自动把它们识别为用户对象的属性。

Ruby Rails学习中:User 模型,验证用户数据第1张

注:你可能还记得, 在上面的代码中, 我们使用下面的命令生成了 Users 控制器和 new 动作$ rails generate controller Users new

创建模型有个类似的命令 —— generate model 。我们可以使用这个命令生成 User 模型, 以及 name 和 email 属性, 如下代码所示。

(1).生成 User 模型

Ruby Rails学习中:User 模型,验证用户数据第2张

注:控制器名是复数, 模型名是单数: 控制器是 Users , 而模型是 User 。我们指定了可选的参数 name:string 和 email:string , 告诉 Rails 我们需要的两个属性是什么, 以及各自的类型(两个都是字符串)。

执行上述 generate 命令之后, 会生成一个迁移文件。迁移是一种递进修改数据库结构的方式, 可以根据需求修改数据模型。执行上述 generate 命令后会自动为 User 模型创建迁移, 这个迁移的作用是创建一个 users 表, 以及 name 和 email 两个列, 如下代码所示:

(2).User 模型的迁移文件(创建 users 表)

打开文件:db/migrate/[timestamp]_create_users.rb

Ruby Rails学习中:User 模型,验证用户数据第3张

注:迁移文件名前面有个时间戳(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 两个列,分别记录创建用户的时间戳和更新用户的时间戳。(前面有使用这两个列的例子。)这个迁移文件表示的完整数据模型如下图所示。

Ruby Rails学习中:User 模型,验证用户数据第4张

我们可以使用如下的 db:migrate 命令执行这个迁移(这叫“向上迁移”):

$ rails db:migrate

Ruby Rails学习中:User 模型,验证用户数据第5张

注:大多数迁移, 都是可逆的, 也就是说可以使用一个简单的命令“向下迁移”,  撤销之前的操作。这个命令是 db:rollback :

$ rails db:rollback

2.模型文件

(1).刚创建的 User 模型

打开文件:app/models/user.rb

Ruby Rails学习中:User 模型,验证用户数据第6张

注:前面说过, class User < ApplicationRecord 的意思是 User 类继承自 ApplicationRecord 类(而它继承自 ActiveRecord::Base 类), 所以 User 模型自动获得了 ActiveRecord::Base 的所有功能。

3. 创建用户对象

探索数据模型使用的工具是 Rails 控制台。因为我们(还)不想修改数据库中的数据, 所以要在沙盒(sandbox)模式中启动控制台:

$ rails console --sandbox

注:如提示消息所说, “Any modifications you make will be rolled back on exit”, 在沙盒模式下使用控制台, 退出当前会话后, 对数据库做的所有改动都会回归到原来的状态(即撤销)。

在前面的控制台会话中, 我们要引入必要的代码才能使用 User.new 创建用户对象。对模型来说, 情况有所不同。你可能还记得前面说过, Rails 控制台会自动加载 Rails 环境, 这其中就包括模型。也就是说, 现在无需加载任何代码就可以直接创建用户对象:

Ruby Rails学习中:User 模型,验证用户数据第7张

注:上述代码显示了用户对象在控制台中的默认表述

如果不为 User.new 指定参数, 对象的所有属性值都是 nil 。前面, 我们自己编写的 User 类可以接受一个散列参数, 指定用于初始化对象的属性。这种方式是受 Active Record 启发的, 在 Active Record 中也可以使用相同的方式指定初始值:

Ruby Rails学习中:User 模型,验证用户数据第8张

注:我们看到 name 和 email 属性的值都已经按预期设定了。

数据的有效性(validity)对理解 Active Record 模型对象很重要, 我们会在以后深入探讨。不过注意, 现在这个 user 对象是有效的, 我们可以在这个对象上调用 valid? 方法确认:

Ruby Rails学习中:User 模型,验证用户数据第9张

目前为止, 我们都没有修改数据库: User.new 只在内存中创建一个对象, user.valid? 只是检查对象是否有效。如果想把用户对象保存到数据库中, 要在 user 变量上调用 save 方法:

Ruby Rails学习中:User 模型,验证用户数据第10张

注:如果保存成功, save 方法返回 true , 否则返回 false 。(现在所有保存操作都会成功, 因为还没有数据验证; 等到了下面的内容就会看到一些失败的例子。) Rails 还会在控制台中显示 user.save 对应的 SQL 语句( INSERT INTO "users"... ), 以供参考。我们几乎不会使用原始的 SQL, 所以此后我会省略 SQL。不过, 从 Active Record各种操作生成的 SQL 中可以学到很多东西。

与以前定义的 User 类一样, User 模型的实例也可以使用点号获取属性

Ruby Rails学习中:User 模型,验证用户数据第11张

等以后会介绍, 虽然一般习惯把创建和保存分成如上所示的两步完成, 不过 Active Record 也允许我们使用User.create 方法把这两步合成一步:

Ruby Rails学习中:User 模型,验证用户数据第12张

Ruby Rails学习中:User 模型,验证用户数据第13张

注:User.create 的返回值不是 true 或 false , 而是创建的用户对象, 可以直接赋值给变量(例如上面第二个命令中的 foo 变量)

create 的逆操作是 destroy :

Ruby Rails学习中:User 模型,验证用户数据第14张

奇怪的是, destroy 和 create 一样, 返回值是对象。我不觉得什么地方会用到 destroy 的返回值。更奇怪的是, 销毁的对象还在内存中

Ruby Rails学习中:User 模型,验证用户数据第15张

注:那么我们怎么知道对象是否真被销毁了呢? 对于已经保存而没有销毁的对象, 怎样从数据库中读取呢? 要回答这些问题, 我们要先学习如何使用 Active Record 查找用户对象。

4.查找用户对象

Active Record 提供了好几种查找对象的方法。下面我们使用这些方法查找前面创建的第一个用户, 同时也验证一下第三个用户( foo )是否被销毁了。先看一下还存在的用户:

Ruby Rails学习中:User 模型,验证用户数据第16张

注:我们把用户的 ID 传给 User.find 方法, Active Record 会返回 ID 为 1 的用户对象

下面来看一下 ID 为 3 的用户是否还在数据库中:

Ruby Rails学习中:User 模型,验证用户数据第17张

注:因为我们在前面销毁了第三个用户, 所以 Active Record 无法在数据库中找到这个用户, 从而抛出一个异常(exception), 这说明在查找过程中出现了问题。因为 ID 不存在, 所以 find 方法抛出 ActiveRe-cord::RecordNotFound 异常。

除了这种查找方式之外, Active Record 还支持通过属性查找用户(find_by)

Ruby Rails学习中:User 模型,验证用户数据第18张

注:我们将使用电子邮件地址做用户名, 在学习如何让用户登录网站时会用到这种 find 方法(后面的内容)。你可能会担心如果用户数量过多, 使用 find_by 的效率不高。事实的确如此, 我们会在下面说明这个问题, 以及如何使用数据库索引解决。

再介绍几个常用的查找方法。首先是 first 方法:

Ruby Rails学习中:User 模型,验证用户数据第19张

很明显, first 会返回数据库中的第一个用户。还有 all 方法:

Ruby Rails学习中:User 模型,验证用户数据第20张

从控制台的输出可以看出, User.all 方法返回一个 ActiveRecord::Relation 实例, 其实这是一个数组, 包含数据库中的所有用户

5.更新用户对象

创建对象后, 一般都会进行更新操作。更新有两种基本方式, 其一, 可以分别为各个属性赋值, 在前面的内容中就是这么做的:

Ruby Rails学习中:User 模型,验证用户数据第21张

注:如果想把改动写入数据库, 必须执行最后一个方法。

我们可以执行 reload 命令来看一下没保存的话是什么情况。 reload 方法会使用数据库中的数据重新加载对象

Ruby Rails学习中:User 模型,验证用户数据第22张

现在我们已经更新了用户数据,如前面所说,现在自动创建的那两个时间戳属性不一样了:

Ruby Rails学习中:User 模型,验证用户数据第23张

更新数据的第二种常用方式是使用 update_attributes 方法

Ruby Rails学习中:User 模型,验证用户数据第24张

update_attributes 方法接受一个指定对象属性的散列作为参数, 如果操作成功, 会执行更新和保存两个操作(保存成功时返回 true )

注:如果任何一个数据验证失败了, 例如存储记录时需要密码, update_attributes 操作就会失败。

如果只需要更新单个属性, 可以使用 update_attribute 方法, 跳过验证:

Ruby Rails学习中:User 模型,验证用户数据第25张

二. 验证用户数据

创建的 User 模型现在已经有了可以使用的 name 和 email 属性, 不过功能还很简单:任何字符串(包括空字符串)都可以使用。名字和电子邮件地址的格式显然要复杂一些。例如, name 不应该是空的, email 应该符合特定的格式。而且, 我们将把电子邮件地址当成用户名用来登录, 那么在数据库中就不能重复。

1.有效性测试

前面说过, TDD 并不适用所有情况, 但是模型验证是使用 TDD 的绝佳时机。如果不先编写失败测试, 再想办法让它通过, 我们很难确定验证是否实现了我们希望实现的功能。

我们采用的方法是, 先得到一个有效的模型对象, 然后把属性改为无效值, 以此确认这个对象是无效的。以防万一, 我们先编写一个测试, 确认模型对象一开始是有效的。这样, 如果验证测试失败了, 我们才知道的确事出有因(而不是因为一开始对象是无效的)。

前面代码中的命令生成了一个用于测试 User 模型的测试文件, 现在这个文件中还没什么内容, 如下图所示:

(1).还没什么内容的 User 模型测试文件

打开文件:test/models/user_test.rb

Ruby Rails学习中:User 模型,验证用户数据第26张

为了测试有效的对象,我们要在特殊的 setup 方法中创建一个有效的用户对象 @user 。前面有提到过, setup 方法会在每个测试方法运行前执行。因为 @user 是实例变量, 所以自动可在所有测试方法中使用, 而且我们可以使用 valid? 方法检查它是否有效。

测试如下图所示:

(2).测试用户对象一开始是有效的 GREEN

打开文件:test/models/user_test.rb

Ruby Rails学习中:User 模型,验证用户数据第27张

上图中使用简单的 assert 方法, 如果 @user.valid? 返回 true , 测试就能通过:返回 false , 测试则会失败。

因为 User 模型现在还没有任何验证,所有这个测试可以通过:

Ruby Rails学习中:User 模型,验证用户数据第28张

注:这里, 我们使用 rails test:models 命令, 只运行模型测试(与前面的 rails test:integration 对比一下)。

2.存在性验证

存在性验证算是最基本的验证了, 只是检查指定的属性是否存在。现在我们会确保用户存入数据库之前, name 和 email 字段都有值。以后会介绍如何把这个限制应用到创建用户的注册表单中。

我们要先在前面写好的测试文件的基础上再编写一个测试, 检查 name 属性是否存在。如下图所示, 我们只需把 @user 变量的 name 属性设为空字符串(包含几个空格的字符串), 然后使用 assert_not 方法确认得到的用户对象是无效的。

(1).测试 name 属性的验证 RED

打开文件:test/models/user_test.rb

Ruby Rails学习中:User 模型,验证用户数据第29张

现在,模型测试应该失败:

Ruby Rails学习中:User 模型,验证用户数据第30张

我们在前面应该见过, name 属性的存在性验证使用 validates 方法,而且其参数为 presence: true , 如下图所示。 presence: true 是只有一个元素的可选散列参数; 前面说过, 如果方法的最后一个参数是散列, 可以省略花括号。(前面曾说过,Rails 经常使用散列做参数。)

(2).为 name 属性添加存在性验证 GREEN

打开文件:app/models/user.rb

Ruby Rails学习中:User 模型,验证用户数据第31张

上图中的代码看起来可能有点儿神奇, 其实 validates 就是个方法。加入括号后, 可以写成:

Ruby Rails学习中:User 模型,验证用户数据第32张

打开控制台, 看一下在 User 模型中加入验证后有什么效果:

Ruby Rails学习中:User 模型,验证用户数据第33张

这里我们使用 valid? 方法检查 user 变量的有效性, 如果有一个或多个验证失败, 返回值为 false ; 如果所有验证都能通过, 返回 true 。现在只有一个验证, 所以我们知道是哪一个失败, 不过看一下失败时生成的 er-rors 对象还是很有用的:

Ruby Rails学习中:User 模型,验证用户数据第34张

因为用户无效, 如果尝试把它保存到数据库中, 操作会失败:

Ruby Rails学习中:User 模型,验证用户数据第35张

加入验证后, 测试应该可以通过了:

Ruby Rails学习中:User 模型,验证用户数据第36张

(3).测试 email 属性的验证 RED

打开文件:test/models/user_test.rb

Ruby Rails学习中:User 模型,验证用户数据第37张

(4).为 email 属性添加存在性验证 GREEN

打开文件:app/models/user.rb

Ruby Rails学习中:User 模型,验证用户数据第38张

现在,存在性验证都添加了,测试组件应该可以通过了:

Ruby Rails学习中:User 模型,验证用户数据第39张

3.长度验证

我们已经对 User 模型可接受的数据做了一些限制, 现在必须为用户提供一个名字, 不过我们应该做进一步限制, 因为用户的名字会在演示应用中显示, 所以最好限制它的长度(name:50为上限, email:244为上限)

(1).测试 name 属性的长度验证 RED

打开文件:test/models/user_test.rb

Ruby Rails学习中:User 模型,验证用户数据第40张

现在测试, 应该是失败的:

Ruby Rails学习中:User 模型,验证用户数据第41张

为了让测试通过, 我们要使用验证参数限制长度, 即 length , 以及限制上线的 maximum 参数, 如下图所示:

(2).为 name 属性添加长度验证 GREEN

Ruby Rails学习中:User 模型,验证用户数据第42张

 现在测试应该可以通过了:

Ruby Rails学习中:User 模型,验证用户数据第43张

4.格式验证

电子邮件地址格式验证有点棘手, 而且容易出错, 所以我们会先编写检查有效电子邮件地址的测试, 这些测试应该能通过, 以此捕获验证可能出现的错误。也就是说, 添加验证后, 不仅要拒绝无效的电子邮件地址, 例如 user@163,com,还得接受有效的电子邮件地址, 例如 user@163.com。(显然目前会接受所有电子邮件地址, 因为只要不为空值都能通过验证。)检查有效电子邮件地址的测试如下图所示:

(1).测试有效的电子邮件地址格式 GREEN

打开文件:test/models/user_test.rb

Ruby Rails学习中:User 模型,验证用户数据第44张

注意,我们为 assert 方法指定了可选的第二个参数, 用于定制错误消息, 识别是哪个地址导致测试失败的:
assert @user.valid?, "#{valid_address.inspect} should be valid"
这行代码在字符串插值中使用了前面介绍的 inspect 方法。像这种使用 each 方法的测试, 最好能知道是哪个地址导致失败的, 因为不管哪个地址导致测试失败, 都无法看到行号, 很难查出问题的根源。
接下来, 我们要测试几个无效的电子邮件, 确认它们无法通过验证, 例如 user@example,com(点号变成了逗号)和 user_at_foo.qq.com(没有“@”符号)。与上图一样, 下图也指定了错误消息参数, 识别是哪个地址导致测试失败的。

(2).测试电子邮件地址格式验证 RED

打开文件:test/models/user_test.rb

Ruby Rails学习中:User 模型,验证用户数据第45张

现在, 测试应该失败:

Ruby Rails学习中:User 模型,验证用户数据第46张

电子邮件地址格式验证使用 format 参数, 用法如下:

validates :email, format: { with: /<regular expression>/ }

它使用指定的正则表达式(regular expression, 简称 regex)验证属性。正则表达式很强大, 使用模式匹配字符串, 但往往晦涩难懂。我们要编写一个正则表达式, 匹配有效的电子邮件地址, 但不匹配无效的地址。在官方标准中其实有一个正则表达式, 可以匹配全部有效的电子邮件地址, 但没必要使用这么复杂的正则表达式。我们使用一个更务实的正则表达式, 能很好地满足实际需求, 如下所示:

VALID_EMAIL_REGEX = /A[w+-.]+@[a-zd-.]+.[a-z]+z/i

Ruby Rails学习中:User 模型,验证用户数据第47张

(3).使用正则表达式验证电子邮件地址的格式 GREEN

打开文件:app/models/user.rb

Ruby Rails学习中:User 模型,验证用户数据第48张

其中, VALID_EMAIL_REGEX 是一个常量(constant)。在 Ruby 中常量的首字母为大写。下面这段代码:

  VALID_EMAIL_REGEX = /A[w+-.]+@[a-zd-.]+.[a-z]+z/i
  validates :email, presence: true, length: {maximum: 255}, format: {with: VALID_EMAIL_REGEX}

注:确保只有匹配正则表达式的电子邮件地址才是有效的。这个正则表达式有一个缺陷:能匹配 foo@qq..com 这种有连续点号的地址。修正这个瑕疵需要一个更复杂的正则表达式, 这里我就懒得弄了, 你到网上粘一个下来就可以了

现在测试应该可以通过了:

Ruby Rails学习中:User 模型,验证用户数据第49张

那么, 现在就只剩一个限制要实现了:确保电子邮件地址的唯一性。

5.唯一性验证

我们要先编写一些简短的测试。之前的模型测试, 只是使用 User.new 在内存中创建一个 Ruby 对象, 但是测试唯一性时要把数据存入数据库。对重复电子邮件地址的测试如下图所示:

(1).拒绝重复电子邮件地址的测试 RED

打开文件:test/models/user_test.rb

Ruby Rails学习中:User 模型,验证用户数据第50张

我们使用 @user.dup 方法创建一个和 @user 的电子邮件地址一样的用户对象,然后保存 @user , 因为数据库中的 @user 已经占用了这个电子邮件地址, 所以 duplicate_user 对象无效。

在 email 属性的验证中加入 uniqueness: true 可以让上图中的测试通过, 如下图所示:

(2).电子邮件地址唯一性验证 GREEN

打开文件:app/models/user.rb

Ruby Rails学习中:User 模型,验证用户数据第51张

这还不行,一般来说电子邮件地址不区分大小写,也就说 foo@QQ.com 和 FOO@QQ.COM 或 FoO@Qq.coM 是同一个地址, 所以验证时也要考虑这种情况。

因此, 还要测试不区分大小写, 如下图所示:

(3).测试电子邮件地址的唯一性验证不区分大小写 RED

打开文件:test/models/user_test.rb

Ruby Rails学习中:User 模型,验证用户数据第52张

上面的代码, 在字符串上调用 upcase 方法。这个测试和前面对重复电子邮件的测试作用一样, 只是把地址转换成全部大写字母的形式。如果你觉得太抽象, 那就在控制台中实操一下吧:

Ruby Rails学习中:User 模型,验证用户数据第53张

当然, 现在 duplicate_user.valid? 的返回值是 true , 因为唯一性验证还区分大小写。我们希望得到的结果是 false 。幸好 :uniqueness 可以指定 :case_sensitive 选项, 正好可以解决这个问题, 如下图所示:

(3).电子邮件地址唯一性验证,不区分大小写 GREEN

打开文件:app/models/user.rb

Ruby Rails学习中:User 模型,验证用户数据第54张

注:我们直接把 true 换成了 case_sensitive: false , Rails 会自动指定 :uniqueness 的值为 true 。
至此, 我们的应用虽然还有不足, 但基本可以保证电子邮件地址的唯一性了, 测试组件应该可以通过了:

Ruby Rails学习中:User 模型,验证用户数据第55张

现在还有一个小问题——Active Record 中的唯一性验证无法保证数据库层也能实现唯一性。我来解释一下:

1. 我使用 wahaha@163.com 在演示应用中注册;
2. 不小心按了两次提交按钮,连续发送了两次请求;
3. 然后就会发生这种事情:请求 1 在内存中新建了一个用户对象,能通过验证;请求 2 也一样。请求 1 创建的用户存入了数据库,请求 2 创建的用户也存入了数据库。
4. 结果是,尽管有唯一性验证,数据库中还是有两条用户记录的电子邮件地址是一样的。

注:上面这种难以置信的情况可能发生, 只要有一定的访问量, 在任何 Rails 网站中都可能发生(这是我从教训中学到的经验)。幸好解决的方法很简单, 只需在数据库层也加上唯一性限制。我们要做的是在数据库中为 email 列建立索引,然后为索引加上唯一性约束。

Ruby Rails学习中:User 模型,验证用户数据第56张Ruby Rails学习中:User 模型,验证用户数据第57张
在数据库中创建列时要考虑是否需要通过这个列查找记录。以前面代码中的迁移创建的 email 属性为例,等以后实现登录功能后,我们将根据提交的电子邮件地址查找对应的用户记录。可是,在这个简单的数据模型中通过电子邮件地址查找用户只有一种方法——检查数据库中的所有用户记录,比较记录中的 email 属性和指定的电子邮件地址。也就是说,可能要检查每一条记录(毕竟用户可能是数据库中的最后一条记录)。在数据库领域,这叫全表扫描(full-table scan)。如果网站中有几千个用户,这可不是一件轻松的事。
为 email 列加上索引可以解决这个问题。我们可以把数据库索引看成书籍的索引。如果要在一本书中找出某个字符串(例如 "foobar" )出现的所有位置,需要翻看书中的每一页。但是如果有索引的话,只需在索引中找到 "foobar" 条目,就能看到所有包含 "foobar" 的页码。数据库索引基本上也是这种原理。
数据库索引

为 email 列建立索引要改变数据模型, 在 Rails 中可以通过迁移实现。在前面我们知道了, 生成 User 模型时会自动创建一个迁移文件。现在我们是要改变已经存在的模型结构, 那么使用 migration 命令直接创建迁移文件就可以了:

$ rails generate migration add_index_to_users_email

与 User 模型的迁移不同, 实现电子邮件地址唯一性的操作没有事先定义好的模板可用, 所以我们要自己动手编写, 如下图所示:

(4).添加电子邮件唯一性约束的迁移

打开文件:db/migrate/[timestamp]_add_index_to_users_email.rb

Ruby Rails学习中:User 模型,验证用户数据第58张

注:上述代码调用了 Rails 中的 add_index 方法, 为 users 表中的 email 列建立索引。索引本身并不能保证唯一性, 所以还要指定 unique: true 。

最后, 执行数据库迁移:

$ rails db:migrate

注:如果迁移失败的话,退出所有打开的沙盒模式控制台会话试试。这些会话可能会锁定数据库,拒绝迁移操作。

现在测试组件应该无法通过, 因为固件(fixture)中的数据违背了唯一性约束。固件的作用是为测试数据库提供示例数据。执行创建数据库的命令时会自动生成用户固件, 如下图所示, 电子邮件地址有重复。(电子邮件地址也无效, 但固件中的数据不会应用验证规则。)

(5).默认生成的用户固件 RED

打开文件:test/fixtures/users.yml

Ruby Rails学习中:User 模型,验证用户数据第59张

我们现在还用不到固件, 现在暂且把其中的数据删除, 只留下一个空文件, 如代码清单 6.31 所示:

Ruby Rails学习中:User 模型,验证用户数据第60张

为了保证电子邮件地址的唯一性, 还要做些修改。有些数据库适配器的索引区分大小写, 会把“Foo@QQ.CoM”和“foo@qq.com”视作不同的字符串, 但我们的应用会把它们看做同一个地址。为了避免不兼容, 我们要统一使用小写形式的地址, 存入数据库前, 把“Foo@QQ.CoM”转换成“foo@qq.com”。为此, 我们要使用回调(callback), 在 Active Record 对象生命周期的特定时刻调用。这里, 我们要使用的回调是 before_save , 在用户对象存入数据库之前把电子邮件地址转换成全小写字母形式, 如下图所示。(这只是初步实现方式, 以会再次完善这个东西, 届时会使用常用的“方法引用”定义回调。)

(6).把 email 属性的值转换为小写形式,确保电子邮件地址的唯一性 GREEN

打开文件:app/models/user.rb

Ruby Rails学习中:User 模型,验证用户数据第61张

注:before_save 后有一个块,块中的代码调用字符串的 downcase 方法,把用户的电子邮件地址转换成小写形式。

在上图中,我们可以把赋值语句写成:
self.email = self.email.downcase
其中 self 表示当前用户。但是在 User 模型中,右侧的 self 关键字是可选的,我们在 palindrome? 方法中调用 reverse 方法时说过:
self.email = email.downcase
注意,左侧的 self 不能省略,所以写成
email = email.downcase
是不对的。
现在,前面遇到的问题解决了,数据库会存储请求 1 创建的用户,不会存储请求 2 创建的用户,因为后者违反了唯一性约束。(在 Rails 的日志中会显示一个错误,不过并无大碍。)为 email 列建立索引同时也解决了:为 email 列添加索引之后,使用电子邮件地址查找用户时不会进行全表扫描,从而解决了潜在的效率问题。

(7).上图中把电子邮件地址转换成小写形式的测试 GREEN

打开文件:test/models/user_test.rb

Ruby Rails学习中:User 模型,验证用户数据第62张

(8). before_save 回调的另一种实现方式 GREEN

Ruby Rails学习中:User 模型,验证用户数据第63张

。。。

累了,累了,歇会

免责声明:文章转载自《Ruby Rails学习中:User 模型,验证用户数据》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇npm 常用的命令elk日志收集架构下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

数据库SQL优化大总结之 百万级数据库优化方案(转载)

网上关于SQL优化的教程很多,但是比较杂乱。近日有空整理了一下,写出来跟大家分享一下,其中有错误和不足的地方,还请大家纠正补充。 这篇文章我花费了大量的时间查找资料、修改、排版,希望大家阅读之后,感觉好的话推荐给更多的人,让更多的人看到、纠正以及补充。   一、百万级数据库优化方案 1.对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 o...

SHELL脚本编程循环篇-while循环

              SHELL脚本编程循环篇-while循环                                           作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任。 一.while循环的语法格式 while CONDITION; do   循环体 done 以上参数关键点说明:   C...

01、MySQL_简介

数据库概念   数据库(Database)是按照数据结构来组织、存储和管理数据的建立在计算机存储设备上的仓库。   数据库:存储数据的仓库 数据库分类 网络数据库   网络数据库是指把数据库技术引入到计算机网络系统中,借助于网络技术将存储于数据库中的大量信息及时发布出去;而计算机网络借助于成熟的数据库技术对网络中的各种数据进行有效管理,并实现用户与网络中的...

装mongodb在centos7上

1、下载安装包 curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.2.12.tgz 2、解压 tar -zxvf mongodb-linux-x86_64-3.2.12.tgz 3、移动到指定位置 mv mongodb-linux-x86_64-3.2.12/ /usr/loc...

jmeter(八)-JDBC请求(sqlserver)

做JDBC请求,首先要了解这个JDBC对象是什么,然后寻找响应的数据库连接URL和数据库驱动。 数据库URL:jdbc:sqlserver://200.99.197.190:1433;databaseName=ebank 数据库驱动:com.microsoft.sqlserver.jdbc.SQLServerDriver 下载sqljdbc4.jar放在...

Linux 安装SonarQube

            1、在安装SonarQube 之前需要先了解一下它是做什么的   SonarQube助力于让所有开发人员编写更干净、更安全的代码   SonarQube是一个用于管理代码质量的开放平台,可以快速的定位代码中潜在的或者明显的错误。目前支持Java,C#,C/C++,Python,PL/SQL,Cobol,JavaScrip,Groov...