Setting a default value for a Schema in changeset

I want to generate a name to a user in changeset if it was not provided. What would be a good way to do that?

I figured I should check that the name was not provided in changes, and that the user does not already have a name. But perhaps there is a simpler way?

defmodule App.Users.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name])
    |> add_name_if_missing()
    |> validate_required([:name])
  end

  defp add_name_if_missing(%Ecto.Changeset{changes: %{name: _}} = changeset) do
    changeset
  end

  defp add_name_if_missing(%Ecto.Changeset{data: %App.Users.User{name: nil}} = changeset) do
    changeset
    |> put_change(:name, generate_new_name())
  end

  defp add_name_if_missing(changeset) do
    changeset
  end
end

Use in migration the default option

1 Like

If you need the name to be dynamically generated then yours is the easiest way. Otherwise, you can use a default option on the field as in https://hexdocs.pm/ecto/Ecto.Schema.html#field/3

2 Likes

Indeed, if you want to dynamically generate the name, your way is already quite good.

1 Like

Hello, I’m quite new to elixir, could you explain why there are 3 pattern options and what each of them does? I have a similar case in my code, but I’m not so deep into Ecto yet.

Thanks.

The logic expressed by those three pattern-matching function heads goes like this:

  1. If we’re currently updating the name field, then we’ve already got it handled. No need to do anything extra just now.
  2. Otherwise, if we see that the already-stored name is nil (i.e. there isn’t already a name stored for the user), we generate a name and put that as a change in the changeset.
  3. Otherwise, the user already has a name, so again, no need to do anything.

Hi!

If possible, let your database handle it by setting a default in the migration (with Migration.add/3).

If you need some custom logic to generate the default value, use either OP’s solution or the autogenerate option of Schema.field/3.

I personally don’t see a reason to ever use the default option of Schema.field/3.

Thanka you very much. That explained everything

TBH there are better ways to write this code using Ecto.Changeset APIs instead of explicit pattern-matching on Changeset internals.

My attempt (not executed, so may contain bugs):

defp add_name_if_missing(changeset) do
  case get_field(changeset, :name) do
    nil -> put_change(changeset, :name, generate_new_name())
    _ -> changeset
  end
end

Ecto.Changeset.get_field looks in both changes and data to find the given field.

One minor difference: this code runs generate_new_name when changing name from non-nil to nil. That may be a feature :man_shrugging:

2 Likes

I think, this is the best and simplest solution.I tried it and it is working with me.

Fyi, you can generalize this fairly nicely:

  defp dynamic_default(changeset, key, value_fun) do
    case get_field(changeset, key) do
      nil -> put_change(changeset, key, value_fun.())
      _ -> changeset
    end
  end
2 Likes