<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # 6.2 用戶數據驗證 [6.1 節](#user-model)創建的用戶模型現在已經有了可以使用的 `name` 和 `email` 屬性,不過功能還很簡單:任何字符串(包括空字符串)都可以使用。名字和電子郵件地址的格式顯然要復雜一些。例如,`name` 不應該是空的,`email` 應該符合特定的格式。而且,我們要把電子郵件地址當成用戶名用來登錄,那么在數據庫中就不能重復出現。 總之,`name` 和 `email` 不是什么字符串都可以使用的,我們要對它們可使用的值做個限制。Active Record 通過數據驗證實現這種限制([2.3.2 節](chapter2.html#putting-the-micro-in-microposts)簡單提到過)。本節,我們會介紹幾種常用的數據驗證:存在性、長度、格式和唯一性。[6.3.2 節](#user-has-secure-password)還會介紹另一種常用的數據驗證——二次確認。[7.3 節](chapter7.html#unsuccessful-signups)會看到,如果提交了不合要求的數據,數據驗證會顯示一些很有用的錯誤消息。 ## 6.2.1 有效性測試 [旁注 3.3](chapter3.html#aside-when-to-test)說過,TDD 并不適用所有情況,但是模型驗證是使用 TDD 的絕佳時機。如果不先編寫失敗測試,再想辦法讓它通過,我們很難確定驗證是否實現了我們希望實現的功能。 我們采用的方法是,先得到一個有效的模型對象,然后把屬性改為無效值,以此確認這個對象是無效的。以防萬一,我們先編寫一個測試,確認模型對象一開始是有效的。這樣,如果驗證測試失敗了,我們才知道的確事出有因(而不是因為一開始對象是無效的)。 [代碼清單 6.1](#listing-generate-user-model) 中的命令生成了一個用來測試用戶模型的測試文件,現在這個文件中還沒什么內容,如[代碼清單 6.4](#listing-default-user-test) 所示。 ##### 代碼清單 6.4:還沒什么內容的用戶模型測試文件 test/models/user_test.rb ``` require 'test_helper' class UserTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end ``` 為了測試有效的對象,我們要在特殊的 `setup` 方法中創建一個有效的用戶對象 `@user`。[3.6 節](chapter3.html#mostly-static-pages-exercises)的練習中提到過,`setup` 方法會在每個測試方法運行前執行。因為 `@user` 是實例變量,所以自動可在所有測試方法中使用,而且我們可以使用 `valid?` 方法檢查它是否有效。測試如[代碼清單 6.5](#listing-valid-user-test) 所示。 ##### 代碼清單 6.5:測試用戶對象一開始是有效的 GREEN test/models/user_test.rb ``` require 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end test "should be valid" do assert @user.valid? end end ``` [代碼清單 6.5](#listing-valid-user-test) 使用簡單的 `assert` 方法,如果 `@user.valid?` 返回 `true`,測試就能通過;返回 `false`,測試則會失敗。 因為用戶模型現在還沒有任何驗證,所有這個測試可以通過: ##### 代碼清單 6.6:**GREEN** ``` $ bundle exec rake test:models ``` 這里,我們使用 `rake test:models`,只運行模型測試(和 [5.3.4 節](chapter5.html#layout-link-tests)的 `rake test:integration` 對比一下)。 ## 6.2.2 存在性驗證 存在性驗證算是最基本的驗證了,只是檢查指定的屬性是否存在。本節我們會確保用戶存入數據庫之前,`name` 和 `email` 字段都有值。[7.3.3 節](chapter7.html#signup-error-messages)會介紹如何把這個限制應用到創建用戶的注冊表單中。 我們要先在[代碼清單 6.5](#listing-valid-user-test) 的基礎上再編寫一個測試,檢查 `name` 屬性是否存在。如[代碼清單 6.7](#listing-name-presence-test) 所示,我們只需把 `@user` 的 `name` 屬性設為空字符串(包含幾個空格的字符串),然后使用 `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: "user@example.com") 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 節](chapter2.html#a-toy-app-exercises)中見過,`name` 屬性的存在性驗證使用 `validates` 方法,而且其參數為 `presence: true`,如[代碼清單 6.9](#listing-validates-presence-of-name) 所示。`presence: true` 是只有一個元素的可選哈希參數,[4.3.4 節](chapter4.html#css-revisited)說過,如果方法的最后一個參數是哈希,可以省略花括號。([5.1.1 節](chapter5.html#site-navigation)說過,Rails 經常使用哈希做參數。) ##### 代碼清單 6.9:添加 `name` 屬性存在性驗證 GREEN app/models/user.rb ``` class User < ActiveRecord::Base validates :name, presence: true end ``` [代碼清單 6.9](#listing-validates-presence-of-name) 中的代碼看起來可能有點兒神奇,其實 `validates` 就是個方法。加入括號后,可以寫成: ``` class User < ActiveRecord::Base validates(:name, presence: true) end ``` 打開控制臺,看一下在用戶模型中加入驗證后有什么效果:[[10](#fn-10)] ``` $ rails console --sandbox >> user = User.new(name: "", email: "mhartl@example.com") >> user.valid? => false ``` 這里我們使用 `valid?` 方法檢查 `user` 變量的有效性,如果有一個或多個驗證失敗,返回值為 `false`,如果所有驗證都能通過,返回 `true`。現在只有一個驗證,所以我們知道是哪一個失敗,不過看一下失敗時生成的 `errors` 對象還是很有用的: ``` >> user.errors.full_messages => ["Name can't be blank"] ``` (錯誤消息暗示,Rails 使用 [4.4.3 節](chapter4.html#modifying-built-in-classes)介紹的 `blank?` 方法驗證存在性。) 因為用戶無效,如果嘗試把它保存到數據庫中,操作會失敗: ``` >> user.save => false ``` 加入驗證后,[代碼清單 6.7](#listing-name-presence-test) 中的測試應該可以通過了: ##### 代碼清單 6.10:**GREEN** ``` $ bundle exec rake test:models ``` 按照[代碼清單 6.7](#listing-name-presence-test) 的方式,再編寫一個檢查 `email` 屬性存在性的測試就簡單了,如[代碼清單 6.11](#listing-email-presence-test) 所示。讓這個測試通過的應用代碼如[代碼清單 6.12](#listing-validates-presence-of-email) 所示。 ##### 代碼清單 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: "user@example.com") 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](#listing-length-validation-test) 所示。 ##### 代碼清單 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: "user@example.com") 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 aaaaaaaaaaa@example.com" >> ("a" * 244 + "@example.com").length => 256 ``` 現在,[代碼清單 6.14](#listing-length-validation-test) 中的測試應該失敗: ##### 代碼清單 6.15:**RED** ``` $ bundle exec rake test ``` 為了讓測試通過,我們要使用驗證參數限制長度,即 `length`,以及限制上線的 `maximum` 參數,如[代碼清單 6.16](#listing-length-validation) 所示。 ##### 代碼清單 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` 屬性需要更復雜的限制,必須是有效地電子郵件地址才行。目前我們只拒絕空電子郵件地址,本節我們要限制電子郵件地址符合常用的形式,類似 `user@example.com` 這種。 這里我們用到的測試和驗證不是十全十美的,只是剛好可以覆蓋大多數有效的電子郵件地址,并拒絕大多數無效的電子郵件地址。我們會先測試一組有效的電子郵件地址和一組無效的電子郵件地址。我們要使用 `%w[]` 創建這兩組地址,其中每個地址都是字符串形式,如下面的控制臺會話所示: ``` >> %w[foo bar baz] => ["foo", "bar", "baz"] >> addresses = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp] => ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"] >> addresses.each do |address| ?> puts address >> end USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp ``` 在上面這個控制臺會話中,我們使用 `each` 方法([4.3.2 節](chapter4.html#blocks))遍歷 `addresses` 數組中的元素。掌握這種用法之后,我們就可以編寫一些基本的電子郵件地址格式驗證測試了。 電子郵件地址格式認證有點棘手,且容易出錯,所以我們會先編寫檢查有效電子郵件地址的測試,這些測試應該能通過,以此捕獲驗證可能出現的錯誤。也就是說,添加驗證后,不僅要拒絕無效的電子郵件地址,例如 _user@example,com_,還得接受有效的電子郵件地址,例如 _user@example.com_。(顯然目前會接受所有電子郵件地址,因為只要不為空值都能通過驗證。)檢查有效電子郵件地址的測試如[代碼清單 6.18](#listing-email-format-valid-tests) 所示。 ##### 代碼清單 6.18:測試有效的電子郵件地址格式 GREEN test/models/user_test.rb ``` require 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end . . . test "email validation should accept valid addresses" do valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn] 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 節](chapter4.html#hashes-and-symbols) 介紹的 `inspect` 方法。像這種使用 `each` 的測試,最好能知道是哪個地址導致失敗的,因為不管哪個地址導致測試失敗,都無法看到行號,很難查出問題的根源。 接下來,我們要測試一系列無效的電子郵件,確認它們無法通過驗證,例如 _user@example,com_(點號變成了逗號)和 _user_at_foo.org_(沒有“@”符號)。和[代碼清單 6.18](#listing-email-format-valid-tests) 一樣,[代碼清單 6.19](#listing-email-format-validation-tests) 中也指定了錯誤消息參數,識別是哪個地址導致測試失敗的。 ##### 代碼清單 6.19:測試電子郵件地址格式驗證 RED test/models/user_test.rb ``` require 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") 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](#fn-11)]本書使用一個更務實的正則表達式,能很好地滿足實際需求,如下所示: ``` VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i ``` 為了便于理解,我把 `VALID_EMAIL_REGEX` 拆分成幾塊來講,如[表 6.1](#table-valid-email-regex) 所示。 表 6.1:拆解匹配有效電子郵件地址的正則表達式 | 表達式 | 含義 | | --- | --- | | `/\A[\w+\-.]@[a-z\d\-.]\.[a-z]+\z/i` | 完整的正則表達式 | | `/` | 正則表達式開始 | | `\A` | 匹配字符串的開頭 | | `[\w+\-.]+` | 一個或多個字母、加號、連字符、或點號 | | `@` | 匹配 @ 符號 | | `[a-z\d\-.]+` | 一個或多個字母、數字、連字符或點號 | | `\.` | 匹配點號 | | `[a-z]+` | 一個或多個字母 | | `\z` | 匹配字符串結尾 | | `/` | 結束正則表達式 | | `i` | 不區分大小寫 | 從[表 6.1](#table-valid-email-regex) 中雖然能學到很多,但若想真正理解正則表達式,我覺得交互式正則表達式匹配程序,例如 [Rubular](http://www.rubular.com/)([圖 6.6](#fig-rubular))[[12](#fn-12)],是必不可少的的。Rubular 的界面很友好,便于編寫所需的正則表達式,而且還有一個便捷的語法速查表。我建議你使用 Rubular 來理解[表 6.1](#table-valid-email-regex)中的正則表達式——讀得次數再多也不比不上在 Rubular 中實操幾次。(注意:如果你在 Rubular 中輸入[表 6.1](#table-valid-email-regex) 中的正則表達式,要把 `\A` 和 `\z` 去掉,因為 Rubular 無法正確處理字符串的頭尾。) ![rubular](https://box.kancloud.cn/2016-05-11_5732bd0c1cf85.png)圖 6.6:強大的 Rubular 正則表達式編輯器 在 `email` 屬性的格式驗證中使用這個表達式后得到的代碼如[代碼清單 6.21](#listing-validates-format-of-email) 所示。 ##### 代碼清單 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 } ``` 確保只有匹配正則表達式的電子郵件地址才是有效的。這個正則表達式有一個缺陷:能匹配 `foo@bar..com` 這種有連續點號的地址。修正這個瑕疵需要一個更復雜的正則表達式,留作練習由你完成([6.5 節](#modeling-users-exercises))。 現在測試應該可以通過了: ##### 代碼清單 6.22:**GREEN** ``` $ bundle exec rake test:models ``` 那么就只剩一個限制要實現了:確保電子郵件地址的唯一性。 ## 6.2.5 唯一性驗證 確保電子郵件地址的唯一性(這樣才能作為用戶名),要使用 `validates` 方法的 `:unique` 參數。提前說明,實現的過程中有一個很大的陷阱,所以不要輕易跳過本節,要認真閱讀。 我們要先編寫一些簡短的測試。之前的模型測試,只是使用 `User.new` 在內存中創建一個 Ruby 對象,但是測試唯一性時要把數據存入數據庫。[[13](#fn-13)]對重復電子郵件地址的測試如[代碼清單 6.23](#listing-validates-uniqueness-of-email-test) 所示。 ##### 代碼清單 6.23:拒絕重復電子郵件地址的測試 RED test/models/user_test.rb ``` require 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") 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](#listing-validates-uniqueness-of-email-test) 中的測試通過,如[代碼清單 6.24](#listing-validates-uniqueness-of-email) 所示。 ##### 代碼清單 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 ``` 這還不行,一般來說電子郵件地址不區分大小寫,也就說 `foo@bar.com` 和 `FOO@BAR.COM` 或 `FoO@BAr.coM` 是同一個地址,所以驗證時也要考慮這種情況。[[14](#fn-14)]因此,還要測試不區分大小寫,如[代碼清單 6.25](#listing-validates-uniqueness-of-email-case-insensitive-test) 所示。 ##### 代碼清單 6.25:測試電子郵件地址的唯一性驗證不區分大小寫 RED test/models/user_test.rb ``` require 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") 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 節](chapter4.html#blocks)簡介過)。這個測試和前面對重復電子郵件的測試作用一樣,只是把地址轉換成全部大寫字母的形式。如果覺得太抽象,那就在控制臺中實操一下吧: ``` $ rails console --sandbox >> user = User.create(name: "Example User", email: "user@example.com") >> user.email.upcase => "USER@EXAMPLE.COM" >> duplicate_user = user.dup >> duplicate_user.email = user.email.upcase >> duplicate_user.valid? => true ``` 當然,現在 `duplicate_user.valid?` 的返回值是 `true`,因為唯一性驗證還區分大小寫。我們希望得到的結果是 `false`。幸好 `:uniqueness` 可以指定 `:case_sensitive` 選項,正好可以解決這個問題,如[代碼清單 6.26](#listing-validates-uniqueness-of-email-case-insensitive) 所示。 ##### 代碼清單 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 使用 alice@wonderland.com 在演示應用中注冊; 2. Alice 不小心按了兩次提交按鈕,連續發送了兩次請求; 3. 然后就會發生這種事情:請求 1 在內存中新建了一個用戶對象,能通過驗證;請求 2 也一樣。請求 1 創建的用戶存入了數據庫,請求 2 創建的用戶也存入了數據庫。 4. 結果是,盡管有唯一性驗證,數據庫中還是有兩條用戶記錄的電子郵件地址是一樣的。 相信我,上面這種難以置信的情況可能發生,只要有一定的訪問量,在任何 Rails 網站中都可能發生。幸好解決的辦法很容易,只需在數據庫層也加上唯一性限制。我們要做的是在數據庫中為 `email` 列建立索引([旁注 6.2](#aside-database-indices)),然后為索引加上唯一性限制。 ##### 旁注 6.2:數據庫索引 在數據庫中創建列時要考慮是否需要通過這個列查找記錄。以[代碼清單 6.2](#listing-users-migration)中的遷移創建的 `email` 屬性為例,[第 7 章](chapter7.html#sign-up)實現登錄功能后,我們要根據提交的電子郵件地址查找對應的用戶記錄。可是在這個簡單的數據模型中通過電子郵件地址查找用戶只有一種方法——檢查數據庫中的所有用戶記錄,比較記錄中的 `email` 屬性和指定的電子郵件地址。也就是說,可能要檢查每一條記錄(畢竟用戶可能是數據庫中的最后一條記錄)。在數據庫領域,這叫“全表掃描”。如果網站中有幾千個用戶,這可不是一件輕松的事。 在 `email` 列加上索引可以解決這個問題。我們可以把數據庫索引看成書籍的索引。如果要在一本書中找出某個字符串(例如 `"foobar"`)出現的所有位置,需要翻看書中的每一頁。但是如果有索引的話,只需在索引中找到 `"foobar"` 條目,就能看到所有包含 `"foobar"` 的頁碼。數據庫索引基本上也是這種原理。 為 `email` 列建立索引要改變數據模型,在 Rails 中可以通過遷移實現。在 [6.1.1 節](#database-migrations)我們看到,生成用戶模型時會自動創建一個遷移文件([代碼清單 6.2](#listing-users-migration))。現在我們是要改變已經存在的模型結構,那么使用 `migration` 命令直接創建遷移文件就可以了: ``` $ rails generate migration add_index_to_users_email ``` 和用戶模型的遷移不一樣,實現電子郵件地址唯一性的操作沒有事先定義好的模板可用,所以我們要自己動手編寫,如[代碼清單 6.28](#listing-email-uniqueness-index) 所示。[[15](#fn-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](#listing-generate-user-model) 中的命令時會自動生成用戶固件,如[代碼清單 6.29](#listing-default-fixtures) 所示,電子郵件地址有重復。(電子郵件地址也無效,但固件中的數據不會應用驗證規則。) ##### 代碼清單 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 章](chapter8.html#log-in-log-out)才會用到固件,現在暫且把其中的數據刪除,只留下一個空文件,如[代碼清單 6.30](#listing-empty-fixtures) 所示。 ##### 代碼清單 6.30:沒有內容的固件文件 GREEN test/fixtures/users.yml ``` # empty ``` 為了保證電子郵件地址的唯一性,還要做些修改。有些數據庫適配器的索引區分大小寫,會把“Foo@ExAMPle.CoM”和“foo@example.com”視作不同的字符串,但我們的應用會把他們看做同一個地址。為了避免不兼容,我們要統一使用小寫形式的地址,存入數據庫前,把“Foo@ExAMPle.CoM”轉換成“foo@example.com”。為此,我們要使用“回調”(callback),在 Active Record 對象生命周期的特定時刻調用。[[16](#fn-16)]現在,我們要使用的回調是 `before_save`,在用戶存入數據庫之前把電子郵件地址轉換成全小寫字母形式,如[代碼清單 6.31](#listing-email-downcase) 所示。(這只是初步實現方式,[10.1.1 節](chapter10.html#account-activations-resource)會再次討論這個話題,屆時會使用常用的“方法引用”定義回調。) ##### 代碼清單 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](#listing-email-downcase) 中,`before_save` 后有一個塊,塊中的代碼調用字符串的 `downcase` 方法,把用戶的電子郵件地址轉換成小寫形式。(針對電子郵件地址轉換成小寫形式的測試留作[練習](#modeling-users-exercises)。) 在[代碼清單 6.31](#listing-email-downcase) 中,我們可以把賦值語句寫成: ``` self.email = self.email.downcase ``` 其中 `self` 表示當前用戶。但是在用戶模型中,右側的 `self` 關鍵字是可選的,我們在 `palindrome` 方法中調用 `reverse` 方法時說過([4.4.2 節](chapter4.html#class-inheritance)): ``` self.email = email.downcase ``` 注意,左側的 `self` 不能省略,所以寫成 ``` email = email.downcase ``` 是不對的。([8.4 節](chapter8.html#remember-me)會進一步討論這個話題。) 現在,前面 Alice 遇到的問題解決了,數據庫會存儲請求 1 創建的用戶,不會存儲請求 2 創建的用戶,因為后者違反了唯一性約束。(在 Rails 的日志中會顯示一個錯誤,不過無大礙。)為 `email` 列建立索引同時也解決了 [6.1.4 節](#finding-user-objects)提到的問題:如[旁注 6.2](#aside-database-indices) 所說,在 `email` 列上添加索引后,使用電子郵件地址查找用戶時不會進行全表掃描,解決了潛在的效率問題。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看