Phoenix username 已被人占用

2023-12-18 14:17 更新

如果你已完成上一章,你可能已經(jīng)猜到,這章的規(guī)則要怎么寫(xiě),不過(guò)在那之前,還是讓我們先寫(xiě)個(gè)測(cè)試:

diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs
index 4c174ab..47df0c7 100644
--- a/test/tv_recipe/users_test.exs
+++ b/test/tv_recipe/users_test.exs
@@ -20,4 +20,13 @@ defmodule TvRecipe.UserTest do
     attrs = %{@valid_attrs | username: ""}
     assert %{username: ["請(qǐng)?zhí)顚?xiě)"]} = errors_on(%User{}, attrs)
   end
+
+  test "username should be unique" do
+    # 在測(cè)試數(shù)據(jù)庫(kù)中插入新用戶
+    user_changeset = User.changeset(%User{}, @valid_attrs)
+    TvRecipe.Repo.insert! user_changeset
+
+    # 嘗試插入同名用戶,應(yīng)報(bào)告錯(cuò)誤
+    assert {:error, changeset} = TvRecipe.Repo.insert(User.changeset(%User{}, %{@valid_attrs | email: "chenxsan+1@gmail.com"}))
+  end
 end

此時(shí)運(yùn)行 mix test test/tv_recipe/users_test.exs,我們的測(cè)試會(huì)全部通過(guò)。這是因?yàn)?,我們?cè)趫?zhí)行 mix phx.gen.html 命令時(shí),指定了 uniqueusername 字段,這樣生成的 User 結(jié)構(gòu)里,我們已經(jīng)有了唯一性的限定規(guī)則,如下所示:

def changeset(struct, params \\ %{}) do
  struct
  |> cast(params, [:username, :email, :password])
  |> validate_required([:username, :email, :password], message: "請(qǐng)?zhí)顚?xiě)")
  |> unique_constraint(:username)
  |> unique_constraint(:email)
end

但上面的測(cè)試?yán)?,我們只知道插入同名用戶時(shí),Phoenix 會(huì)返回錯(cuò)誤,至于錯(cuò)誤是什么,我們還沒(méi)有檢查。

我們來(lái)完善下我們上面的測(cè)試代碼:

diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs
index 47df0c7..9748671 100644
--- a/test/tv_recipe/users_test.exs
+++ b/test/tv_recipe/users_test.exs
@@ -28,5 +28,8 @@ defmodule TvRecipe.UserTest do

     # 嘗試插入同名用戶,應(yīng)報(bào)告錯(cuò)誤
     assert {:error, changeset} = TvRecipe.Repo.insert(user_changeset)
+
+    # 錯(cuò)誤信息為“用戶名已被人占用”
+    assert %{username: ["用戶名已被人占用"]} = errors_on(changeset)
   end
 end

再次運(yùn)行 mix test test/tv_recipe/users_test.exs 的結(jié)果是:

$ mix test test/tv_recipe/users_test.exs
.

  1) test username should be unique (TvRecipe.UserTest)
     test/tv_recipe/users_test.exs:24
     Assertion with in failed
     code:  %{username: ["用戶名已被人占用"]} = errors_on(changeset)
     left:  %{username: ["用戶名已被人占用"]}
     right: [username: "has already been taken"]
     stacktrace:
       test/tv_recipe/users_test.exs:33: (test)

..

Finished in 0.1 seconds
4 tests, 1 failure

測(cè)試不通過(guò)。因?yàn)?用戶名已被人占用"不等于 "has already been taken"。

這是當(dāng)然,我們還未自定義用戶名重復(fù)時(shí)的提示消息。

打開(kāi) lib/tv_recipe/users/user.ex 文件,做如下修改:

diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex
index 87ce321..88ad2af 100644
--- a/lib/tv_recipe/users/user.ex
+++ b/lib/tv_recipe/users/user.ex
@@ -16,7 +16,7 @@ defmodule TvRecipe.User do
     struct
     |> cast(params, [:username, :email, :password])
     |> validate_required([:username, :email, :password], message: "請(qǐng)?zhí)顚?xiě)")
-    |> unique_constraint(:username)
+    |> unique_constraint(:username, message: "用戶名已被人占用")
     |> unique_constraint(:email)
   end
 end

再跑一次測(cè)試,順利通過(guò)。

結(jié)束這一章了?不不不,還有一點(diǎn),我們或許遺漏了,就是用戶名的大小寫(xiě)。

大小寫(xiě)敏感

我們先寫(xiě)個(gè)測(cè)試驗(yàn)證一下:

diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs
index 9748671..44cb21b 100644
--- a/test/tv_recipe/users_test.exs
+++ b/test/tv_recipe/users_test.exs
@@ -32,4 +32,13 @@ defmodule TvRecipe.UserTest do
     # 錯(cuò)誤信息為“用戶名已被人占用”
     assert %{username: ["用戶名已被人占用"]} = errors_on(changeset)
   end
+
+  test "username should be case insensitive" do
+    user_changeset = User.changeset(%User{}, @valid_attrs)
+    TvRecipe.Repo.insert! user_changeset
+
+    # 嘗試插入大小寫(xiě)不一致的用戶名,應(yīng)報(bào)告錯(cuò)誤
+    another_user_changeset = User.changeset(%User{}, %{@valid_attrs | username: "Chenxsan", email: "chenxsan+1@gmail.com"})
+    assert {:error, changeset} = TvRecipe.Repo.insert(another_user_changeset)
+  end
 end

運(yùn)行測(cè)試的結(jié)果是:

$ mix test test/tv_recipe/users_test.exs
warning: variable "changeset" is unused
  test/tv_recipe/users_test.exs:42

...

  1) test username should be case insensitive (TvRecipe.UserTest)
     test/tv_recipe/users_test.exs:36
     match (=) failed
     code:  {:error, changeset} = TvRecipe.Repo.insert(another_user_changeset)
     right: {:ok,
             %TvRecipe.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
              email: "chenxsan+1@gmail.com", id: 36,
              inserted_at: ~N[2017-01-24 11:57:43.741097],
              password: "some content",
              updated_at: ~N[2017-01-24 11:57:43.741109], username: "Chenxsan"}}
     stacktrace:
       test/tv_recipe/users_test.exs:42: (test)

.

Finished in 0.1 seconds
5 tests, 1 failure

我們的判斷錯(cuò)了。無(wú)論是 chenxsan 還是 Chenxsan 的用戶名,我們都插入成功,這當(dāng)然不是我們期望的結(jié)果。

我們來(lái)看看 unique_constraint 文檔的一段說(shuō)明

Unfortunately, different databases provide different guarantees when it comes to case-sensitiveness. For example, in MySQL, comparisons are case-insensitive by default. In Postgres, users can define case insensitive column by using the :citext type/extension.

不同數(shù)據(jù)庫(kù)對(duì)大小寫(xiě)的處理不一樣,比如 MySQL 是大小寫(xiě)不敏感的,而默認(rèn)情況下,PostgreSQL 字段是大小寫(xiě)敏感的,不過(guò)我們可以使用 citext 擴(kuò)展類(lèi)型。

如果不用 citext,文檔中仍有其它辦法:

If for some reason your database does not support case insensitive columns, you can explicitly downcase values before inserting/updating them

根據(jù)提示,我們的 user.ex 代碼可以做如下修改:

diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex
index 88ad2af..fc07824 100644
--- a/lib/tv_recipe/users/user.ex
+++ b/lib/tv_recipe/users/user.ex
@@ -16,6 +16,7 @@ defmodule TvRecipe.User do
     struct
     |> cast(params, [:username, :email, :password])
     |> validate_required([:username, :email, :password], message: "請(qǐng)?zhí)顚?xiě)")
+    |> update_change(:username, &String.downcase/1)
     |> unique_constraint(:username, message: "用戶名已被人占用")
     |> unique_constraint(:email)
   end

再跑一次測(cè)試,測(cè)試通過(guò)。

可是,如果我一定要用 CHenxsan 這個(gè)用戶名呢?String.downcase 的處理方式,導(dǎo)致我們只能使用小寫(xiě)的 chenxsan。

我們還有個(gè)辦法,只是比較復(fù)雜。

數(shù)據(jù)庫(kù)遷移

在用戶注冊(cè)一章,我們用 mix phx.gen.html 生成了許多樣板文件,其中有一條:

* creating priv/repo/migrations/20170123145857_create_user.exs

打開(kāi)該文件,它的內(nèi)容如下:

defmodule TvRecipe.Repo.Migrations.CreateUser do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :username, :string
      add :email, :string
      add :password, :string

      timestamps()
    end
    create unique_index(:users, [:username])
    create unique_index(:users, [:email])

  end
end

正是 create unique_index(:users, [:username]) 一行,在數(shù)據(jù)庫(kù)中限定了 username 的唯一性。

只是它沒(méi)有處理大小寫(xiě)的問(wèn)題。但我們能夠處理處理,只要把它改成如下:

create unique_index(:users, ["lower(username)"])

那么要怎樣去掉舊的 unique_index 而換上新的呢?

Ecto 提供了一個(gè) mix ecto.gen.migration 功能用于這類(lèi)轉(zhuǎn)換。

在命令行下創(chuàng)建一個(gè)試試:

$ cd tv_recipe
$ mix ecto.gen.migration alter_user_username_index
* creating priv/repo/migrations
* creating priv/repo/migrations/20170124123616_alter_user_username_index.exs

打開(kāi)新創(chuàng)建的 20170124123616_alter_user_username_index.exs 文件,做如下修改:

diff --git a/priv/repo/migrations/20170124123616_alter_user_username_index.exs b/priv/repo/migrations/20170124123616_alter_user_username_index.exs
index 5723a10..4060abf 100644
--- a/priv/repo/migrations/20170124123616_alter_user_username_index.exs
+++ b/priv/repo/migrations/20170124123616_alter_user_username_index.exs
@@ -2,6 +2,7 @@ defmodule TvRecipe.Repo.Migrations.AlterUserUsernameIndex do
   use Ecto.Migration

   def change do
+    drop index(:users, [:username]) # 移除舊索引
+    create unique_index(:users, ["lower(username)"]) # 增加新索引
   end
 end

然后在命令行中執(zhí)行 mix ecto.migrate,把遷移文件的修改落實(shí)到數(shù)據(jù)庫(kù)中:

$ mix ecto.migrate

20:39:44.900 [info]  == Running TvRecipe.Repo.Migrations.AlterUserUsernameIndex.change/0 forward

20:39:44.900 [info]  drop index users_username_index

20:39:44.930 [info]  create index users_lower_username_index

20:39:44.940 [info]  == Migrated in 0.0s

最后要記得將此前 user.ex 文件中 String.downcase 的修改撤銷(xiāo)掉:

diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex
index fc07824..88ad2af 100644
--- a/lib/tv_recipe/users/user.ex
+++ b/lib/tv_recipe/users/user.ex
@@ -16,7 +16,6 @@ defmodule TvRecipe.User do
     struct
     |> cast(params, [:username, :email, :password])
     |> validate_required([:username, :email, :password], message: "請(qǐng)?zhí)顚?xiě)")
-    |> update_change(:username, &String.downcase/1)
     |> unique_constraint(:username, message: "用戶名已被人占用")
     |> unique_constraint(:email)
   end

再運(yùn)行測(cè)試看看:

mix test test/tv_recipe/users_test.exs
warning: variable "changeset" is unused
  test/tv_recipe/users_test.exs:42

.

  1) test username should be case insensitive (TvRecipe.UserTest)
     test/tv_recipe/users_test.exs:36
     ** (Ecto.ConstraintError) constraint error when attempting to insert struct:

         * unique: users_lower_username_index

     If you would like to convert this constraint into an error, please
     call unique_constraint/3 in your changeset and define the proper
     constraint name. The changeset defined the following constraints:

         * unique: users_email_index
         * unique: users_username_index

     stacktrace:
       (ecto) lib/ecto/repo/schema.ex:493: anonymous fn/4 in Ecto.Repo.Schema.constraints_to_errors/3
       (elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
       (ecto) lib/ecto/repo/schema.ex:479: Ecto.Repo.Schema.constraints_to_errors/3
       (ecto) lib/ecto/repo/schema.ex:213: anonymous fn/13 in Ecto.Repo.Schema.do_insert/4
       test/tv_recipe/users_test.exs:42: (test)

.

  2) test username should be unique (TvRecipe.UserTest)
     test/tv_recipe/users_test.exs:24
     ** (Ecto.ConstraintError) constraint error when attempting to insert struct:

         * unique: users_lower_username_index

     If you would like to convert this constraint into an error, please
     call unique_constraint/3 in your changeset and define the proper
     constraint name. The changeset defined the following constraints:

         * unique: users_email_index
         * unique: users_username_index

     stacktrace:
       (ecto) lib/ecto/repo/schema.ex:493: anonymous fn/4 in Ecto.Repo.Schema.constraints_to_errors/3
       (elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
       (ecto) lib/ecto/repo/schema.ex:479: Ecto.Repo.Schema.constraints_to_errors/3
       (ecto) lib/ecto/repo/schema.ex:213: anonymous fn/13 in Ecto.Repo.Schema.do_insert/4
       test/tv_recipe/users_test.exs:30: (test)

.

Finished in 0.1 seconds
5 tests, 2 failures

情況變得更糟糕了,報(bào)告了 2 個(gè)錯(cuò)誤。這是因?yàn)樗饕Q已經(jīng)改變,而我們的代碼還在使用默認(rèn)的舊索引名。我們需要在 unique_constraint 里明確指出索引名稱:

diff --git a/lib/tv_recipe/users/user.ex b/lib/tv_recipe/users/user.ex
index 88ad2af..08e4054 100644
--- a/lib/tv_recipe/users/user.ex
+++ b/lib/tv_recipe/users/user.ex
@@ -16,7 +16,7 @@ defmodule TvRecipe.User do
     struct
     |> cast(params, [:username, :email, :password])
     |> validate_required([:username, :email, :password], message: "請(qǐng)?zhí)顚?xiě)")
-    |> unique_constraint(:username, message: "用戶名已被人占用")
+    |> unique_constraint(:username, name: :users_lower_username_index, message: "用戶名已被人占用")
     |> unique_constraint(:email)
   end
 end

再跑一遍測(cè)試:

$ mix test test/tv_recipe/users_test.exs
warning: variable "changeset" is unused
  test/tv_recipe/users_test.exs:42

.....

Finished in 0.1 seconds
5 tests, 0 failures

悉數(shù)通過(guò)。

但眼尖的你可能已經(jīng)注意到,我們的測(cè)試報(bào)告里有一條:

warning: variable "changeset" is unused

在 Elixir 下,如果有定義的變量未曾使用到,編譯時(shí)就會(huì)出現(xiàn)警告。

上面這條警告對(duì)應(yīng)的是測(cè)試代碼中的這一行:

assert {:error, changeset} = TvRecipe.Repo.insert(another_user_changeset)

我們只斷定了插入數(shù)據(jù)庫(kù)失敗,還沒(méi)有檢查 changeset 里的錯(cuò)誤。

讓我們完善下測(cè)試:

diff --git a/test/tv_recipe/users_test.exs b/test/tv_recipe/users_test.exs
index 9451c2d..975c7b1 100644
--- a/test/tv_recipe/users_test.exs
+++ b/test/tv_recipe/users_test.exs
@@ -40,5 +40,6 @@ defmodule TvRecipe.UserTest do
     # 嘗試插入大小寫(xiě)不一致的用戶名,應(yīng)報(bào)告錯(cuò)誤
     another_user_changeset = User.changeset(%User{}, %{@valid_attrs | username: "Chenxsan", email: "chenxsan+1@gmail.com"})
     assert {:error, changeset} = TvRecipe.Repo.insert(another_user_changeset)
+    assert %{username: ["用戶名已被人占用"]} = errors_on(changeset)
   end
 end

再次運(yùn)行測(cè)試,悉數(shù)通過(guò)。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)