变更集验证 #
一、Changeset概述 #
1.1 什么是Changeset #
Changeset是Ecto的核心概念,用于:
- 数据类型转换
- 数据验证
- 错误收集
- 数据库操作准备
1.2 Changeset结构 #
elixir
%Ecto.Changeset{
data: %User{},
changes: %{name: "John"},
errors: [],
valid?: true,
required: [:email, :name],
types: %{email: :string, name: :string}
}
二、创建Changeset #
2.1 基本Changeset #
elixir
defmodule Hello.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :name, :string
field :age, :integer
field :active, :boolean, default: true
timestamps()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :name, :age, :active])
|> validate_required([:email, :name])
end
end
2.2 cast函数 #
elixir
cast(struct, attrs, allowed_fields, opts \\ [])
- struct: Schema结构体
- attrs: 参数映射
- allowed_fields: 允许更新的字段
- opts: 选项
elixir
user
|> cast(attrs, [:email, :name, :age])
|> validate_required([:email, :name])
2.3 多种Changeset #
elixir
defmodule Hello.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :name, :string
field :password, :string, virtual: true
field :hashed_password, :string
field :role, :string
timestamps()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :name, :role])
|> validate_required([:email, :name])
end
def registration_changeset(user, attrs) do
user
|> changeset(attrs)
|> cast(attrs, [:password])
|> validate_required([:password])
|> validate_length(:password, min: 8)
|> put_hashed_password()
end
def update_changeset(user, attrs) do
user
|> changeset(attrs)
|> validate_inclusion(:role, ["user", "admin"])
end
defp put_hashed_password(changeset) do
case get_change(changeset, :password) do
nil -> changeset
password ->
put_change(changeset, :hashed_password, Bcrypt.hash_pwd_salt(password))
end
end
end
三、验证函数 #
3.1 validate_required #
elixir
changeset
|> validate_required([:email, :name])
|> validate_required([:email, :name], message: "This field is required")
3.2 validate_format #
elixir
changeset
|> validate_format(:email, ~r/@/)
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "Invalid email format")
3.3 validate_length #
elixir
changeset
|> validate_length(:name, min: 2)
|> validate_length(:name, max: 100)
|> validate_length(:name, min: 2, max: 100)
|> validate_length(:bio, min: 10, max: 500)
3.4 validate_number #
elixir
changeset
|> validate_number(:age, greater_than: 0)
|> validate_number(:age, greater_than_or_equal_to: 0)
|> validate_number(:price, less_than: 1000)
|> validate_number(:price, less_than_or_equal_to: 1000)
|> validate_number(:price, equal_to: 100)
3.5 validate_inclusion #
elixir
changeset
|> validate_inclusion(:status, ["active", "inactive", "pending"])
|> validate_inclusion(:role, ["user", "admin"], message: "Invalid role")
3.6 validate_exclusion #
elixir
changeset
|> validate_exclusion(:username, ["admin", "root", "system"])
3.7 validate_subset #
elixir
changeset
|> validate_subset(:tags, ["elixir", "phoenix", "ecto"])
3.8 validate_acceptance #
elixir
changeset
|> validate_acceptance(:terms_of_service)
|> validate_acceptance(:privacy_policy, message: "You must accept the privacy policy")
3.9 validate_confirmation #
elixir
changeset
|> validate_confirmation(:password)
|> validate_confirmation(:password, message: "Passwords do not match")
四、约束验证 #
4.1 unique_constraint #
elixir
changeset
|> unique_constraint(:email)
|> unique_constraint(:email, message: "Email already taken")
|> unique_constraint([:email, :username], name: :users_email_username_index)
4.2 foreign_key_constraint #
elixir
changeset
|> foreign_key_constraint(:post_id)
|> foreign_key_constraint(:post_id, message: "Post does not exist")
4.3 assoc_constraint #
elixir
changeset
|> assoc_constraint(:user)
|> assoc_constraint(:user, message: "User does not exist")
4.4 check_constraint #
elixir
changeset
|> check_constraint(:price, name: :price_must_be_positive, message: "Price must be positive")
4.5 exclusion_constraint #
elixir
changeset
|> exclusion_constraint(:date, name: :appointments_date_exclusion)
五、自定义验证 #
5.1 validate_change #
elixir
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :name])
|> validate_required([:email, :name])
|> validate_change(:email, fn :email, email ->
if banned_email?(email) do
[email: "This email domain is not allowed"]
else
[]
end
end)
end
defp banned_email?(email) do
banned_domains = ["temp.com", "fake.com"]
domain = email |> String.split("@") |> List.last()
domain in banned_domains
end
5.2 自定义验证函数 #
elixir
def validate_password_strength(changeset, field) do
validate_change(changeset, field, fn ^field, password ->
cond do
String.length(password) < 8 ->
[{field, "Password must be at least 8 characters"}]
not Regex.match?(~r/[A-Z]/, password) ->
[{field, "Password must contain at least one uppercase letter"}]
not Regex.match?(~r/[a-z]/, password) ->
[{field, "Password must contain at least one lowercase letter"}]
not Regex.match?(~r/[0-9]/, password) ->
[{field, "Password must contain at least one number"}]
true ->
[]
end
end)
end
六、修改数据 #
6.1 put_change #
elixir
changeset
|> put_change(:slug, generate_slug(changeset.changes.title))
|> put_change(:status, "draft")
6.2 update_change #
elixir
changeset
|> update_change(:email, &String.downcase/1)
|> update_change(:name, &String.trim/1)
6.3 delete_change #
elixir
changeset
|> delete_change(:password)
6.4 force_change #
elixir
changeset
|> force_change(:updated_at, DateTime.utc_now())
七、获取数据 #
7.1 get_change #
elixir
email = get_change(changeset, :email)
email = get_change(changeset, :email, "default@example.com")
7.2 get_field #
elixir
name = get_field(changeset, :name)
name = get_field(changeset, :name, "Anonymous")
7.3 fetch_change #
elixir
case fetch_change(changeset, :email) do
{:ok, email} -> handle_email_change(email)
:error -> no_email_change()
end
7.4 fetch_field #
elixir
case fetch_field(changeset, :name) do
{:changes, name} -> name
{:data, name} -> name
:error -> nil
end
八、错误处理 #
8.1 添加错误 #
elixir
changeset
|> add_error(:email, "already taken")
|> add_error(:base, "Something went wrong")
8.2 获取错误 #
elixir
errors = changeset.errors
valid? = changeset.valid?
errors = traverse_errors(changeset, fn {msg, opts} ->
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
8.3 错误格式化 #
elixir
def translate_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end
九、关联Changeset #
9.1 cast_assoc #
elixir
defmodule Hello.Blog.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :title, :string
has_many :comments, Hello.Blog.Comment
end
def changeset(post, attrs) do
post
|> cast(attrs, [:title])
|> cast_assoc(:comments)
end
end
9.2 cast_embed #
elixir
defmodule Hello.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
embeds_one :address, Hello.Accounts.Address
end
def changeset(user, attrs) do
user
|> cast(attrs, [:email])
|> cast_embed(:address)
end
end
9.3 put_assoc #
elixir
def create_post_with_author(attrs, user) do
%Post{}
|> Post.changeset(attrs)
|> put_assoc(:author, user)
|> Repo.insert()
end
十、使用Changeset #
10.1 插入数据 #
elixir
def create_user(attrs) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
10.2 更新数据 #
elixir
def update_user(user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
10.3 在控制器中使用 #
elixir
def create(conn, %{"user" => user_params}) do
case Accounts.create_user(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User created successfully")
|> redirect(to: ~p"/users/#{user.id}")
{:error, changeset} ->
render(conn, :new, changeset: changeset)
end
end
10.4 在模板中使用 #
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
十一、总结 #
11.1 核心函数 #
| 函数 | 说明 |
|---|---|
| cast/3 | 类型转换 |
| validate_required/2 | 必填验证 |
| validate_* | 各种验证 |
| put_change/3 | 设置字段值 |
| add_error/3 | 添加错误 |
11.2 验证类型 #
| 验证 | 说明 |
|---|---|
| validate_required | 必填 |
| validate_format | 格式 |
| validate_length | 长度 |
| validate_number | 数字范围 |
| validate_inclusion | 包含于 |
| validate_exclusion | 排除于 |
| unique_constraint | 唯一约束 |
11.3 下一步 #
现在你已经了解了变更集验证,接下来让我们学习 Channel基础,深入了解实时通信!
最后更新:2026-03-28