Set default value at the database level, getting nil in my struct after Repo.insert

The migration and schema:

alter table(:users) do
  add :onboarded, :boolean, default: false
end

schema "users" do
  * snip *
  field :onboarded, :boolean
  * snip *
end

I have a simple onboarding changeset:

def onboarding_email_changeset(struct, params) do
  struct
  |> cast(params, [:email])
  |> validate_required([:email])
  |> validate_format(:email, ~r/@/)
  |> unique_constraint(:email)
  |> MyApp.User.Onboarding.put_confirmation_code
end

And I use it to save a new User record.

def create_user_with_email(email) do
  changeset = User.onboarding_email_changeset(%User{}, %{email: email})
  case Repo.insert(changeset) do
    {:ok, user} ->
      user
    _ ->
      {:error, "User already exists with that email"}
  end
end

However the user struct I get back doesn’t have the default value for onboarded set. It’s just nil, despite the default value being set correctly in my database.

# In postgresql:
# iex -S mix
# MyApp.Repo.all(MyApp.User)
[%MyApp.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  confirmation_code: "574767", email: "a@email.com", encrypted_password: nil,
  fullname: nil, id: 1, inserted_at: ~N[2017-06-30 16:51:55.251189],
  onboarded: false, password: nil,
  teams: #Ecto.Association.NotLoaded<association :teams is not loaded>,
  updated_at: ~N[2017-06-30 16:51:55.258862]}]

# The struct in Elixir:
%MyApp.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
 confirmation_code: "522475", email: "sergio@email.com",
 encrypted_password: nil, fullname: nil, id: 73,
 inserted_at: ~N[2017-06-30 17:03:07.724691], onboarded: nil, password: nil,
 teams: #Ecto.Association.NotLoaded<association :teams is not loaded>,
 updated_at: ~N[2017-06-30 17:03:07.724702]}

Would appreciate any help, thank!

If you want what the database stores (instead of what was ‘sent’ to the database) you need to do a query to get it back, or you need to add returned to the fields you are returned (PostgreSQL supports those I think, but not MySQL/MariaDB?).

I fixed it by adding default to my schema.

field :onboarded, :boolean, default: false

A little more explicit than what I’m used to from Rails, but all in all, much less magic and clearer. :thumbsup:

That turns it from using the ‘database default’ to using an ‘application default’, which would be returned yes (as it would be sent to the database). :slight_smile:

You can use the read_after_writes option on fields if you have some that are modified by the database after they are written.

3 Likes

Would you recommend read_after_writes or default for this case? It sounds like read_after_writes is more correct as it’s literally using the value given by the database.

If the default is set in the database and you always need the value back then use read_after_writes, if you set it on the application side then use default, if you set it in the database but rarely need it back then just query it back if needed. :slight_smile: