变更集验证 #

一、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