How to update a belongs_to association?

Hi,

It looks like Ecto prevents, by default, to change an association because on_update is :raiseby default.

I naively changed my belongs_to association and added on_update: :update as suggested by the documentation but I now have another error:

Since you have set `:on_replace` to `:update`, you are only allowed
to update the existing entry by giving updated fields as a map or
keyword list or set it to nil.

If you indeed want to replace the existing :type, you have
to change the foreign key field directly.

I don’t really understand what that means because:
a) I pass my changes in a params parameter that is a map
b) it looks Ecto is suggesting me to change the value directly by specifying the database column, which sounds awkward.

So I tried the latter and set the TYP_ID column directly (ex: %{TYP_ID: 42}) with the new id. This did work but that means I need to reference database columns directly from my controller code, which looks to be a bad practice in my opinion.

What code do I need to change the id of an association in a table? This is how I expected it to be done but did not work:

Persitance:

  schema "upload" do
    field :filename, :string, source: :UPL_FILENAME, size: 500
    field :request_uuid, :string, source: :UPL_REQUEST_UUID, size: 100
    field :status_code, :string, source: :UPL_STATUS_CODE, size: 50
    belongs_to :type, Type, foreign_key: :TYP_ID, on_replace: :update
    timestamps()
  end

  def changeset(%{status_code: status_code} = upload, params \\ %{}) do
    upload
    |> change(params)
    |> cast(params, [:filename, :request_uuid, :status_code])
    |> validate_required([:filename])
    |> validate_required([:request_uuid])
    |> validate_required([:status_code])
  end
  
  def save_upload(%Upload{} = upload, params \\ %{}) do
    changeset(upload, params)
    |> Repo.insert_or_update()
  end

Insert or update:

    upload =
      Uploads.get_upload(String.to_integer(upload_id))
      |> Repo.preload(:type)
    updated_type = Uploads.get_type(new_type.id)
    Upload.save_upload(upload, %{type: updated_type})

This code works when I update the first time (when the column is null) but not on subsequent updates. How can I handle this case with Ecto?

Thanks.

Why do you call change/2?

changeset function gets a struct and and a map with changes as parameters. change/2 creates a changeset for the struct with the given changes. Do I miss something?

Yes definitely. cast/4 creates a changeset from untrusted data (user input). You call change/2 for creating a changeset from trusted/internal data. You use either one or the other for transforming the params into a changeset. Here I suppose the parameters received are untrusted, so you just need to use cast/4. This doesn’t answer your question but that’s a first issue in the code:)

1 Like