如果你已完成上一章,你可能已經(jīng)猜到,這章的規(guī)則要怎么寫,不過在那之前,還是讓我們先寫個測試:
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: ["請?zhí)顚?]} = errors_on(%User{}, attrs)
end
+
+ test "username should be unique" do
+ # 在測試數(shù)據(jù)庫中插入新用戶
+ user_changeset = User.changeset(%User{}, @valid_attrs)
+ TvRecipe.Repo.insert! user_changeset
+
+ # 嘗試插入同名用戶,應報告錯誤
+ assert {:error, changeset} = TvRecipe.Repo.insert(User.changeset(%User{}, %{@valid_attrs | email: "chenxsan+1@gmail.com"}))
+ end
end
此時運行 mix test test/tv_recipe/users_test.exs
,我們的測試會全部通過。這是因為,我們在執(zhí)行 mix phx.gen.html
命令時,指定了 unique
給 username
字段,這樣生成的 User
結構里,我們已經(jīng)有了唯一性的限定規(guī)則,如下所示:
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:username, :email, :password])
|> validate_required([:username, :email, :password], message: "請?zhí)顚?)
|> unique_constraint(:username)
|> unique_constraint(:email)
end
但上面的測試里,我們只知道插入同名用戶時,Phoenix 會返回錯誤,至于錯誤是什么,我們還沒有檢查。
我們來完善下我們上面的測試代碼:
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
# 嘗試插入同名用戶,應報告錯誤
assert {:error, changeset} = TvRecipe.Repo.insert(user_changeset)
+
+ # 錯誤信息為“用戶名已被人占用”
+ assert %{username: ["用戶名已被人占用"]} = errors_on(changeset)
end
end
再次運行 mix test test/tv_recipe/users_test.exs
的結果是:
$ 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
測試不通過。因為"用戶名已被人占用"不等于 "has already been taken"。
這是當然,我們還未自定義用戶名重復時的提示消息。
打開 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: "請?zhí)顚?)
- |> unique_constraint(:username)
+ |> unique_constraint(:username, message: "用戶名已被人占用")
|> unique_constraint(:email)
end
end
再跑一次測試,順利通過。
結束這一章了?不不不,還有一點,我們或許遺漏了,就是用戶名的大小寫。
大小寫敏感
我們先寫個測試驗證一下:
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
# 錯誤信息為“用戶名已被人占用”
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
+
+ # 嘗試插入大小寫不一致的用戶名,應報告錯誤
+ 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
運行測試的結果是:
$ 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
我們的判斷錯了。無論是 chenxsan
還是 Chenxsan
的用戶名,我們都插入成功,這當然不是我們期望的結果。
我們來看看 unique_constraint
文檔的一段說明:
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ù)庫對大小寫的處理不一樣,比如 MySQL 是大小寫不敏感的,而默認情況下,PostgreSQL 字段是大小寫敏感的,不過我們可以使用 citext 擴展類型。
如果不用 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: "請?zhí)顚?)
+ |> update_change(:username, &String.downcase/1)
|> unique_constraint(:username, message: "用戶名已被人占用")
|> unique_constraint(:email)
end
再跑一次測試,測試通過。
可是,如果我一定要用 CHenxsan
這個用戶名呢?String.downcase
的處理方式,導致我們只能使用小寫的 chenxsan
。
我們還有個辦法,只是比較復雜。
數(shù)據(jù)庫遷移
在用戶注冊一章,我們用 mix phx.gen.html
生成了許多樣板文件,其中有一條:
* creating priv/repo/migrations/20170123145857_create_user.exs
打開該文件,它的內(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ù)庫中限定了 username
的唯一性。
只是它沒有處理大小寫的問題。但我們能夠處理處理,只要把它改成如下:
create unique_index(:users, ["lower(username)"])
那么要怎樣去掉舊的 unique_index
而換上新的呢?
Ecto 提供了一個 mix ecto.gen.migration
功能用于這類轉換。
在命令行下創(chuàng)建一個試試:
$ 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
打開新創(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ù)據(jù)庫中:
$ 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
的修改撤銷掉:
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: "請?zhí)顚?)
- |> update_change(:username, &String.downcase/1)
|> unique_constraint(:username, message: "用戶名已被人占用")
|> unique_constraint(:email)
end
再運行測試看看:
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
情況變得更糟糕了,報告了 2 個錯誤。這是因為索引名稱已經(jīng)改變,而我們的代碼還在使用默認的舊索引名。我們需要在 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: "請?zhí)顚?)
- |> unique_constraint(:username, message: "用戶名已被人占用")
+ |> unique_constraint(:username, name: :users_lower_username_index, message: "用戶名已被人占用")
|> unique_constraint(:email)
end
end
再跑一遍測試:
$ 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ù)通過。
但眼尖的你可能已經(jīng)注意到,我們的測試報告里有一條:
warning: variable "changeset" is unused
在 Elixir 下,如果有定義的變量未曾使用到,編譯時就會出現(xiàn)警告。
上面這條警告對應的是測試代碼中的這一行:
assert {:error, changeset} = TvRecipe.Repo.insert(another_user_changeset)
我們只斷定了插入數(shù)據(jù)庫失敗,還沒有檢查 changeset
里的錯誤。
讓我們完善下測試:
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
# 嘗試插入大小寫不一致的用戶名,應報告錯誤
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
再次運行測試,悉數(shù)通過。
更多建議: