Ecto.build_assoc does not trigger changeset function

Hello team,

I’ve been learning Elixir and Phoenix framework and am on my way to building my first app.

The app is structured as follows:

It has 2 separate contexts:

  1. Accounts context with Organizations and Users models
  2. TechCore context with Applications and Messages models.

The elixir file for the Applications model is as follows:

defmodule MyCoolApp.TechCore.Applications do
  require Logger
  use Ecto.Schema
  import Ecto.Changeset

  schema "applications" do
    field :name, :string
    field :api_token, :string

    belongs_to :organizations, MyCoolApp.Accounts.Organizations
    timestamps()
  end

  @doc false
  def changeset(applications, attrs \\ %{}) do
    applications
    |> cast(attrs, [:name])
    |> set_api_token_if_null()
    |> validate_required([:name, :api_token])

  end

  defp set_api_token_if_null(changeset) do
    token_ = get_field(changeset, :api_token)
    if token_ do
      changeset
    else
      put_change(changeset, :api_token, (:crypto.strong_rand_bytes(90) |> Base.encode64 |> binary_part(0, 80)))
    end
  end
end

The module for Organizations is as follows:


defmodule MyCoolApp.Accounts.Organizations do
  use Ecto.Schema
  import Ecto.Changeset

  schema "organizations" do
    field :name, :string
    field :contact_email, :string

    has_many :users, MyCoolApp.Accounts.Users
    has_many :applications, MyCoolApp.TechCore.Applications
    timestamps()
  end

  @doc false
  def changeset(organizations, attrs) do
    organizations
    |> cast(attrs, [:name, :contact_email])
    |> validate_required([:name, :contact_email])
  end
end

Now comes the part that I do not understand.

When I try to create a new applications record like so:
%Applications{name: "App testing", organizations_id: 2} |> Applications.changeset(%{}) |> Repo.insert

The record is created (since the changeset function gets called).

However, when I try to create a record using build_assoc like so:
Repo.get_by!(Accounts.Organizations, id: 1) |> Ecto.build_assoc(:applications, name: "My New App") |> Repo.insert!

I get a not null violation error as follows:

** (Postgrex.Error) ERROR 23502 (not_null_violation) null value in column "api_token" violates not-null constraint

    table: applications
    column: api_token

Can someone please help me understand what I’m misunderstanding / doing wrong here please?

You’re calling set_api_token_if_null in your changeset, so the api_token column is always being set. You’re not setting that in your build_assoc and your DB evidently has a NOT NULL constraint on that column which is triggering the error in the latter case.

Thanks @smathy for your reply. But this just went a bit over my head. Would you be kind enough to explain this further?

I did not understand the part about "You’re not setting api_token in your build_assoc.

I’m new to Elixir and so please excuse my naivety here.

You see how you have Ecto.build_assoc(:applications, name: "My New App")? The only field of the associated Applications that you’re setting there is name. So when Ecto tries to insert a new DB record into the applications table the only column in the DB that it’s setting is the name column, however that DB table requires the api_token column to have a value too, so in your build_assoc call you’d need to set that field, like: Ecto.build_assoc(:applications, name: "My New App", api_token: "foobar")

The reason it’s working when you call your changeset is because inside that changeset function, your code sets the api_token field if it’s not already set (in the else clause of the set_api_token_if_null function).

1 Like

I understand. Now, if I need to do this as an associated record, wont the changeset function chain get fired? Will validation not happen then?

When I try to build an Organization → Application association.

In this case, will I have to run it as a function on the likes of %Application{api_token: Application.set_api_token_if_null()} ?

No, the latter code you have (Repo.get_by!(Accounts.Organizations, id: 1) |> Ecto.build_assoc(:applications, name: "My New App") |> Repo.insert!) is interacting directly with the Repo/Ecto interfaces, that’s at a lower level than using your changeset.

To use your changeset you would need to be using cast_assoc and Handling nested associations with cast_assoc might help you here too.

1 Like

I think you might be wrongly believing that your changeset function is called when you are directly interacting with the DB / Repo. That’s not the case. You have to make sure you put things through the changeset function first.

Ecto is made to be explicit and non-magical. Which can be made worse by sometimes magical-looking syntax but that’s a matter of learning it and it gets better after. Seems you have that part mostly covered.

2 Likes