Search code examples
elixirecto

Ecto schema default value is not included in changeset changes


Consider the following schema:

defmodule EctoBug.Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do
    field :title, :string, default: "test"

    timestamps()
  end

  def changeset(post, attrs) do
    post
    |> cast(attrs, [:title])
    |> validate_required([:title])
  end
end

If I do

changeset = EctoBug.Post.changeset(%EctoBug.Post{}, %{title: "test"})

title field is not present in changes:

#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #EctoBug.Post<>, valid?: true>

I couldn't find anything on this behavior.

Is it a bug?


Solution

  • Ecto's changeset structure holds changes applied to a schema/struct. The reason the default doesn't appear as a change is because when you do %EctoBug.Post{}, it's populated with the default values you set on the schema. Then when you cast the params, since the original and the cast values for the field are the same, it's not really a change and isn't marked as such.

    If you do

    changeset = EctoBug.Post.changeset(%EctoBug.Post{}, %{title: "test"})
    Ecto.Changeset.get_field(changeset, :title)
    

    You should see that title is set. Although Ecto.Changeset.get_change(changeset, :title) will give you nil since it hasn't been changed from the original struct that was given initially.

    If you provide a different value than the default for the field title you should see that it will be tracked as a change.

    Because it works this way, now if you had a record that you were trying to update instead of creating, if you cast against that record some params that were actually the same as it already had, ecto can skip trying to update the record, since it could see that nothing had changed. In the case of an insert, even if it has no changes, Ecto will attempt to insert it.