2
votes

According to the Phoenix documentation:

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.

What I did

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

What I expected

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

I expected an error because the sms_number is required.

What I actually got

[debug] INSERT INTO "users" ("inserted_at", "updated_at", "email", "sms_number", "username") VALUES ($1, $2, $3, $4, $5) 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}

The record is stored in the database

Environment

  • Elixir 1.2.3
  • Ecto 1.1.4
  • Phoenix Ecto 2.0.1
  • Phoenix 1.1.4

What am I doing wrong?

1

1 Answers

9
votes

When you are trying to directly insert struct defined in model the changeset function is not called at all. %User{} is only a struct from a module and ecto does not know anything about functions defined in this module. You can delete changeset function and Repo.insert will still work.

changeset function is your custom validation code. The docs you have pasted state that another function is called instead: Ecto.Changeset.change/2. It creates a changeset that is valid by default and non of the fields are required.

You can try it yourself:

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

Always run your custom validations before inserting to database and use Repo.insert with a changeset instead of raw struct. It is probably also a good idea to enforce required fields on the database itself with not null constraint in your migration:

add :username, :string, null: false