您如何设置 Ecto 变更集以防止保存缺少必填字段的模型?

How do you set up an Ecto changeset to prevent models missing required fields from being saved?

根据 Phoenix 文档:

insert(Ecto.Schema.t | Ecto.Changeset.t, Keyword.t) :: {:ok, Ecto.Schema.t} | {:error, Ecto.Changeset.t}

Inserts a model or a changeset.

In case a model is given, the model is converted into a changeset with all model non-virtual fields as part of the changeset. This conversion is done by calling Ecto.Changeset.change/2 directly.

In case a changeset is given, the changes in the changeset are merged with the model fields, and all of them are sent to the database.

If any before_insert or after_insert callback is registered in the given model, they will be invoked with the changeset.

It returns {:ok, model} if the model has been successfully inserted or {:error, changeset} if there was a validation or a known constraint error.

我做了什么

defmodule Dollar.User do
    use Dollar.Web, :model

    schema "users" do
        field :username, :string
        field :sms_number, :string
        field :email, :string

        timestamps
    end

    @required_fields ~w(sms_number)
    @optional_fields ~w(username email)

    @doc """
    Creates a changeset based on the `model` and `params`.
    If no params are provided, an invalid changeset is returned
    with no validation performed.
    """
    def changeset(user, params \ :empty) do
        user
        |> cast(params, @required_fields, @optional_fields)
        |> validate_length(:sms_number, is: 10)
        |> unique_constraint(:sms_number)
        |> unique_constraint(:username)
    end
end

如我所料

Repo.insert(%User{})
=> {:error, ... }

我预计会出现错误,因为 sms_number 必需的

我实际得到了什么

[debug] INSERT INTO "users" ("inserted_at", "updated_at", "email", "sms_number", "username") VALUES (, , , , ) RETURNING "id" [{{2016, 3, 19}, {15, 13, 5, 0}}, {{2016, 3, 19}, {15, 13, 5, 0}}, nil, nil, nil] OK query=1.5ms
%Dollar.User{__meta__: #Ecto.Schema.Metadata<:loaded>, email: nil, id: 7,
 inserted_at: #Ecto.DateTime<2016-03-19T15:13:05Z>, sms_number: nil,
 updated_at: #Ecto.DateTime<2016-03-19T15:13:05Z>, username: nil}

记录存储在数据库中

环境

我做错了什么?

当您尝试直接插入模型中定义的结构时,根本不会调用 changeset 函数。 %User{} 只是模块中的一个结构,ecto 对这个模块中定义的函数一无所知。您可以删除 changeset 函数,Repo.insert 仍然有效。

changeset 函数是您自定义的验证码。您粘贴的文档声明调用了另一个函数:Ecto.Changeset.change/2。它创建一个默认情况下有效的变更集,并且不需要任何字段。

你可以自己试试:

changeset = Ecto.Changeset.change %User{}
changeset.required # []
changeset.valid?   # true
new_changeset = User.changeset changeset
new_changeset.required # [:sms_number]
new_changeset.valid?   # false

总是 运行 在插入到数据库之前自定义验证,并使用 Repo.insert 和变更集而不是原始结构。在迁移中使用非空约束在数据库本身上强制执行必填字段可能也是一个好主意:

add :username, :string, null: false