6.2 用户数据验证

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

总之,nameemail 不是什么字符串都可以使用的,我们要对它们可使用的值做个限制。Active Record 通过数据验证实现这种限制(2.3.2 节简单提到过)。本节,我们会介绍几种常用的数据验证:存在性、长度、格式和唯一性。6.3.2 节还会介绍另一种常用的数据验证——二次确认。7.3 节会看到,如果提交了不合要求的数据,数据验证会显示一些很有用的错误消息。

6.2.1 有效性测试

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

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

代码清单 6.1 中的命令生成了一个用来测试用户模型的测试文件,现在这个文件中还没什么内容,如代码清单 6.4 所示。

代码清单 6.4:还没什么内容的用户模型测试文件

test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

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

代码清单 6.5:测试用户对象一开始是有效的 GREEN

test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "[email protected]")
  end

  test "should be valid" do
    assert @user.valid?
  end
end

代码清单 6.5 使用简单的 assert 方法,如果 @user.valid? 返回 true,测试就能通过;返回 false,测试则会失败。

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

代码清单 6.6:GREEN
$ bundle exec rake test:models

这里,我们使用 rake test:models,只运行模型测试(和 5.3.4 节rake test:integration 对比一下)。

6.2.2 存在性验证

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

我们要先在代码清单 6.5 的基础上再编写一个测试,检查 name 属性是否存在。如代码清单 6.7 所示,我们只需把 @username 属性设为空字符串(包含几个空格的字符串),然后使用 assert_not 方法确认得到的用户对象是无效的。

代码清单 6.7:测试 name 属性的验证措施 RED

test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "[email protected]")
  end

  test "should be valid" do
    assert @user.valid?
  end

 test "name should be present" do @user.name = "     " assert_not @user.valid? end end

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

代码清单 6.8:RED
$ bundle exec rake test:models

我们在2.5 节中见过,name 属性的存在性验证使用 validates 方法,而且其参数为 presence: true,如代码清单 6.9 所示。presence: true 是只有一个元素的可选哈希参数,4.3.4 节说过,如果方法的最后一个参数是哈希,可以省略花括号。(5.1.1 节说过,Rails 经常使用哈希做参数。)

代码清单 6.9:添加 name 属性存在性验证 GREEN

app/models/user.rb

class User < ActiveRecord::Base
 validates :name, presence: true end

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

class User < ActiveRecord::Base
  validates(:name, presence: true)
end

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

$ rails console --sandbox
>> user = User.new(name: "", email: "[email protected]")
>> user.valid?
=> false

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

>> user.errors.full_messages
=> ["Name can't be blank"]

(错误消息暗示,Rails 使用 4.4.3 节介绍的 blank? 方法验证存在性。)

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

>> user.save
=> false

加入验证后,代码清单 6.7 中的测试应该可以通过了:

代码清单 6.10:GREEN
$ bundle exec rake test:models

按照代码清单 6.7 的方式,再编写一个检查 email 属性存在性的测试就简单了,如代码清单 6.11 所示。让这个测试通过的应用代码如代码清单 6.12 所示。

代码清单 6.11:测试 email 属性的验证措施 RED

test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "[email protected]")
  end

  test "should be valid" do
    assert @user.valid?
  end

  test "name should be present" do
    @user.name = ""
    assert_not @user.valid?
  end

 test "email should be present" do @user.email = "     " assert_not @user.valid? end end
代码清单 6.12:添加 email 属性存在性验证 GREEN

app/models/user.rb

class User < ActiveRecord::Base
  validates :name,  presence: true
 validates :email, presence: true end

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

代码清单 6.13:GREEN
$ bundle exec rake test

6.2.3 长度验证

我们已经对用户模型可接受的数据做了一些限制,现在必须为用户提供一个名字,不过我们应该做进一步限制,因为用户的名字会在演示应用中显示,所以最好限制它的长度。有了前一节的基础,这一步就简单了。

没有科学的方法确定最大长度是多少,我们就使用 50 作为长度的上限吧,所以要验证 51 个字符超长了。而且,用户的电子邮件地址可能会超过字符串的最大长度限制,这个最大值在很多数据库中都是 255——这种情况虽然很少发生,但也有发生的可能。因为下一节的格式验证无法实现这种限制,所以我们要在这一节实现。测试如代码清单 6.14 所示。

代码清单 6.14:测试 name 属性的长度验证 RED

test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "[email protected]")
  end
  .
  .
  .
  test "name should not be too long" do
    @user.name = "a" * 51
    assert_not @user.valid?
  end

  test "email should not be too long" do
    @user.email = "a" * 244 + "@example.com"
    assert_not @user.valid?
  end
end

为了方便,我们使用字符串连乘生成了一个有 51 个字符的字符串。在控制台中可以看到连乘是什么:

>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51

在电子邮件地址长度的测试中,我们创建了一个比要求多一个字符的地址:

>> "a" * 244 + "@example.com"
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
[email protected]"
>> ("a" * 244 + "@example.com").length
=> 256

现在,代码清单 6.14 中的测试应该失败:

代码清单 6.15:RED
$ bundle exec rake test

为了让测试通过,我们要使用验证参数限制长度,即 length,以及限制上线的 maximum 参数,如代码清单 6.16 所示。

代码清单 6.16:为 name 属性添加长度验证 GREEN

app/models/user.rb

class User < ActiveRecord::Base
 validates :name,  presence: true, length: { maximum: 50 }  validates :email, presence: true, length: { maximum: 255 }
end

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

代码清单 6.17:GREEN
$ bundle exec rake test

测试组件再次通过,接下来我们要实现一个更有挑战的验证——电子邮件地址的格式。

6.2.4 格式验证

name 属性的验证只需做一些简单的限制就好——任何非空、长度小于 51 个字符的字符串都可以。可是 email 属性需要更复杂的限制,必须是有效地电子邮件地址才行。目前我们只拒绝空电子邮件地址,本节我们要限制电子邮件地址符合常用的形式,类似 [email protected] 这种。

这里我们用到的测试和验证不是十全十美的,只是刚好可以覆盖大多数有效的电子邮件地址,并拒绝大多数无效的电子邮件地址。我们会先测试一组有效的电子邮件地址和一组无效的电子邮件地址。我们要使用 %w[] 创建这两组地址,其中每个地址都是字符串形式,如下面的控制台会话所示:

>> %w[foo bar baz]
=> ["foo", "bar", "baz"]
>> addresses = %w[[email protected] [email protected] [email protected]]
=> ["[email protected]", "[email protected]", "[email protected]"]
>> addresses.each do |address|
?>   puts address
>> end
[email protected]
[email protected]
[email protected]

在上面这个控制台会话中,我们使用 each 方法(4.3.2 节)遍历 addresses 数组中的元素。掌握这种用法之后,我们就可以编写一些基本的电子邮件地址格式验证测试了。

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

代码清单 6.18:测试有效的电子邮件地址格式 GREEN

test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "[email protected]")
  end
  .
  .
  .
  test "email validation should accept valid addresses" do
    valid_addresses = %w[[email protected] [email protected] [email protected]
 [email protected] [email protected]]
    valid_addresses.each do |valid_address|
      @user.email = valid_address
      assert @user.valid?, "#{valid_address.inspect} should be valid"
    end
  end
end

注意,我们为 assert 方法指定了可选的第二个参数,定制错误消息,识别是哪个地址导致测试失败的:

assert @user.valid?, "#{valid_address.inspect} should be valid"

这行代码在字符串插值中使用了 4.3.3 节 介绍的 inspect 方法。像这种使用 each 的测试,最好能知道是哪个地址导致失败的,因为不管哪个地址导致测试失败,都无法看到行号,很难查出问题的根源。

接下来,我们要测试一系列无效的电子邮件,确认它们无法通过验证,例如 user@example,com(点号变成了逗号)和 user_at_foo.org(没有“@”符号)。和代码清单 6.18 一样,代码清单 6.19 中也指定了错误消息参数,识别是哪个地址导致测试失败的。

代码清单 6.19:测试电子邮件地址格式验证 RED

test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "[email protected]")
  end
  .
  .
  .
  test "email validation should reject invalid addresses" do
    invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
 foo@bar_baz.com foo@bar+baz.com]
    invalid_addresses.each do |invalid_address|
      @user.email = invalid_address
      assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
    end
  end
end

现在,测试应该失败:

代码清单 6.20:RED
$ bundle exec rake test

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

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

使用指定的正则表达式验证属性。正则表达式很强大,但往往很晦涩,用来模式匹配字符串。所以我们要编写一个正则表达式,匹配有效的电子邮件地址,但不匹配无效的地址。

在官方标准中其实有一个正则表达式,可以匹配全部有效的电子邮件地址,但没必要使用这么复杂的正则表达式。[11]本书使用一个更务实的正则表达式,能很好地满足实际需求,如下所示:

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

为了便于理解,我把 VALID_EMAIL_REGEX 拆分成几块来讲,如表 6.1 所示。

表 6.1:拆解匹配有效电子邮件地址的正则表达式

表达式 含义
/\A[\w+\-.]@[a-z\d\-.]\.[a-z]+\z/i 完整的正则表达式
/ 正则表达式开始
\A 匹配字符串的开头
[\w+\-.]+ 一个或多个字母、加号、连字符、或点号
@ 匹配 @ 符号
[a-z\d\-.]+ 一个或多个字母、数字、连字符或点号
\. 匹配点号
[a-z]+ 一个或多个字母
\z 匹配字符串结尾
/ 结束正则表达式
i 不区分大小写

表 6.1 中虽然能学到很多,但若想真正理解正则表达式,我觉得交互式正则表达式匹配程序,例如 Rubular图 6.6)[12],是必不可少的的。Rubular 的界面很友好,便于编写所需的正则表达式,而且还有一个便捷的语法速查表。我建议你使用 Rubular 来理解表 6.1中的正则表达式——读得次数再多也不比不上在 Rubular 中实操几次。(注意:如果你在 Rubular 中输入表 6.1 中的正则表达式,要把 \A\z 去掉,因为 Rubular 无法正确处理字符串的头尾。)

rubular图 6.6:强大的 Rubular 正则表达式编辑器

email 属性的格式验证中使用这个表达式后得到的代码如代码清单 6.21 所示。

代码清单 6.21:使用正则表达式验证电子邮件地址的格式 GREEN

app/models/user.rb

class User < ActiveRecord::Base
  validates :name,  presence: true, length: { maximum: 50 }
 VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i  validates :email, presence: true, length: { maximum: 255 },
 format: { with: VALID_EMAIL_REGEX } end

其中,VALID_EMAIL_REGEX 是一个常量,在 Ruby 中常量的首字母为大写形式。这段代码:

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

确保只有匹配正则表达式的电子邮件地址才是有效的。这个正则表达式有一个缺陷:能匹配 [email protected] 这种有连续点号的地址。修正这个瑕疵需要一个更复杂的正则表达式,留作练习由你完成(6.5 节)。

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

代码清单 6.22:GREEN
$ bundle exec rake test:models

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

6.2.5 唯一性验证

确保电子邮件地址的唯一性(这样才能作为用户名),要使用 validates 方法的 :unique 参数。提前说明,实现的过程中有一个很大的陷阱,所以不要轻易跳过本节,要认真阅读。

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

代码清单 6.23:拒绝重复电子邮件地址的测试 RED

test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "[email protected]")
  end
  .
  .
  .
 test "email addresses should be unique" do duplicate_user = @user.dup @user.save assert_not duplicate_user.valid? end end

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

email 属性的验证中加入 uniqueness: true 可以让代码清单 6.23 中的测试通过,如代码清单 6.24 所示。

代码清单 6.24:电子邮件地址唯一性验证 GREEN

app/models/user.rb

class User < ActiveRecord::Base
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
 uniqueness: true end

这还不行,一般来说电子邮件地址不区分大小写,也就说 [email protected][email protected][email protected] 是同一个地址,所以验证时也要考虑这种情况。[14]因此,还要测试不区分大小写,如代码清单 6.25 所示。

代码清单 6.25:测试电子邮件地址的唯一性验证不区分大小写 RED

test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "[email protected]")
  end
  .
  .
  .
  test "email addresses should be unique" do
    duplicate_user = @user.dup
 duplicate_user.email = @user.email.upcase    @user.save
    assert_not duplicate_user.valid?
  end
end

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

$ rails console --sandbox
>> user = User.create(name: "Example User", email: "[email protected]")
>> user.email.upcase
=> "[email protected]"
>> duplicate_user = user.dup
>> duplicate_user.email = user.email.upcase
>> duplicate_user.valid?
=> true

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

代码清单 6.26:电子邮件地址唯一性验证,不区分大小写 GREEN

app/models/user.rb

class User < ActiveRecord::Base
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
 uniqueness: { case_sensitive: false } end

注意,我们直接把 true 换成了 case_sensitive: false,Rails 会自动指定 :uniqueness 的值为 true

至此,我们的应用虽还有不足,但基本可以保证电子邮件地址的唯一性了,测试组件应该可以通过了:

代码清单 6.27:GREEN
$ bundle exec rake test

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

  1. Alice 使用 [email protected] 在演示应用中注册;

  2. Alice 不小心按了两次提交按钮,连续发送了两次请求;

  3. 然后就会发生这种事情:请求 1 在内存中新建了一个用户对象,能通过验证;请求 2 也一样。请求 1 创建的用户存入了数据库,请求 2 创建的用户也存入了数据库。

  4. 结果是,尽管有唯一性验证,数据库中还是有两条用户记录的电子邮件地址是一样的。

相信我,上面这种难以置信的情况可能发生,只要有一定的访问量,在任何 Rails 网站中都可能发生。幸好解决的办法很容易,只需在数据库层也加上唯一性限制。我们要做的是在数据库中为 email 列建立索引(旁注 6.2),然后为索引加上唯一性限制。

旁注 6.2:数据库索引

在数据库中创建列时要考虑是否需要通过这个列查找记录。以代码清单 6.2中的迁移创建的 email 属性为例,第 7 章实现登录功能后,我们要根据提交的电子邮件地址查找对应的用户记录。可是在这个简单的数据模型中通过电子邮件地址查找用户只有一种方法——检查数据库中的所有用户记录,比较记录中的 email 属性和指定的电子邮件地址。也就是说,可能要检查每一条记录(毕竟用户可能是数据库中的最后一条记录)。在数据库领域,这叫“全表扫描”。如果网站中有几千个用户,这可不是一件轻松的事。

email 列加上索引可以解决这个问题。我们可以把数据库索引看成书籍的索引。如果要在一本书中找出某个字符串(例如 "foobar")出现的所有位置,需要翻看书中的每一页。但是如果有索引的话,只需在索引中找到 "foobar" 条目,就能看到所有包含 "foobar" 的页码。数据库索引基本上也是这种原理。

email 列建立索引要改变数据模型,在 Rails 中可以通过迁移实现。在 6.1.1 节我们看到,生成用户模型时会自动创建一个迁移文件(代码清单 6.2)。现在我们是要改变已经存在的模型结构,那么使用 migration 命令直接创建迁移文件就可以了:

$ rails generate migration add_index_to_users_email

和用户模型的迁移不一样,实现电子邮件地址唯一性的操作没有事先定义好的模板可用,所以我们要自己动手编写,如代码清单 6.28 所示。[15]

代码清单 6.28:添加电子邮件唯一性约束的迁移

db/migrate/[timestamp]_add_index_to_users_email.rb

class AddIndexToUsersEmail < ActiveRecord::Migration
  def change
 add_index :users, :email, unique: true  end
end

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

最后,执行数据库迁移:

$ bundle exec rake db:migrate

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

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

代码清单 6.29:默认生成的用户固件 RED

test/fixtures/users.yml

# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/
# FixtureSet.html

one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString

我们到第 8 章才会用到固件,现在暂且把其中的数据删除,只留下一个空文件,如代码清单 6.30 所示。

代码清单 6.30:没有内容的固件文件 GREEN

test/fixtures/users.yml

# empty

为了保证电子邮件地址的唯一性,还要做些修改。有些数据库适配器的索引区分大小写,会把“[email protected]”和“[email protected]”视作不同的字符串,但我们的应用会把他们看做同一个地址。为了避免不兼容,我们要统一使用小写形式的地址,存入数据库前,把“[email protected]”转换成“[email protected]”。为此,我们要使用“回调”(callback),在 Active Record 对象生命周期的特定时刻调用。[16]现在,我们要使用的回调是 before_save,在用户存入数据库之前把电子邮件地址转换成全小写字母形式,如代码清单 6.31 所示。(这只是初步实现方式,10.1.1 节会再次讨论这个话题,届时会使用常用的“方法引用”定义回调。)

代码清单 6.31:把 email 属性的值转换为小写形式,确保电子邮件地址的唯一性 GREEN

app/models/user.rb

class User < ActiveRecord::Base
 before_save { self.email = email.downcase }  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end

代码清单 6.31 中,before_save 后有一个块,块中的代码调用字符串的 downcase 方法,把用户的电子邮件地址转换成小写形式。(针对电子邮件地址转换成小写形式的测试留作练习。)

代码清单 6.31 中,我们可以把赋值语句写成:

self.email = self.email.downcase

其中 self 表示当前用户。但是在用户模型中,右侧的 self 关键字是可选的,我们在 palindrome 方法中调用 reverse 方法时说过(4.4.2 节):

self.email = email.downcase

注意,左侧的 self 不能省略,所以写成

email = email.downcase

是不对的。(8.4 节会进一步讨论这个话题。)

现在,前面 Alice 遇到的问题解决了,数据库会存储请求 1 创建的用户,不会存储请求 2 创建的用户,因为后者违反了唯一性约束。(在 Rails 的日志中会显示一个错误,不过无大碍。)为 email 列建立索引同时也解决了 6.1.4 节提到的问题:如旁注 6.2 所说,在 email 列上添加索引后,使用电子邮件地址查找用户时不会进行全表扫描,解决了潜在的效率问题。