Doubts about how to make a upsert with Ecto

I have this schema:

defmodule Pescarte.Domains.ModuloPesquisa.Models.Midia do
  use Pescarte, :model

  alias Pescarte.Domains.Accounts.Models.User
  alias Pescarte.Domains.ModuloPesquisa.Models.Midia.Tag

  @type t :: %Midia{
          id: integer,
          tipo: atom,
          nome_arquivo: binary,
          data_arquivo: Date.t(),
          restrito?: boolean,
          observacao: binary,
          link: binary,
          texto_alternativo: binary,
          id_publico: binary,
          autor: User.t(),
          tags: list(Tag.t())
        }

  @required_fields ~w(tipo nome_arquivo data_arquivo link autor_id)a
  @optional_fields ~w(observacao texto_alternativo restrito?)a

  @tipos ~w(imagem video documento)a

  schema "midia" do
    field :tipo, Ecto.Enum, values: @tipos
    field :nome_arquivo, :string
    field :data_arquivo, :date
    field :restrito?, :boolean, default: false
    field :observacao, :string
    field :link, :string
    field :texto_alternativo, :string
    field :id_publico, :string

    belongs_to :autor, User, on_replace: :update

    many_to_many :tags, Tag,
      join_through: "midias_tags",
      on_replace: :delete,
      unique: true

    timestamps()
  end

  @spec changeset(Midia.t(), map, list(Tag.t())) :: {:ok, Midia.t()} | {:error, changeset}
  def changeset(%__MODULE__{} = midia, attrs, tags \\ []) do
    midia
    |> cast(attrs, @required_fields ++ @optional_fields)
    |> validate_required(@required_fields)
    |> unique_constraint(:link)
    |> unique_constraint(:nome_arquivo)
    |> foreign_key_constraint(:autor_id)
    |> put_assoc(:tags, tags)
    |> put_change(:id_publico, Nanoid.generate())
    |> apply_action(:parse)
  end

  def tipos, do: @tipos
end

And I want to create an upsert function for this schema, tried to do:

defmodule Pescarte.Domains.ModuloPesquisa.MidiaRepository do
  alias Pescarte.Domains.ModuloPesquisa.IManageRepository
  alias Pescarte.Domains.ModuloPesquisa.Models.Midia
  alias Pescarte.Repo

  @behaviour IManageRepository

  # ...

  @impl true
  def upsert(attrs) do
    with {:ok, midia} <- Midia.changeset(attrs) do
      Repo.insert(midia, on_conflict: {:replace_all_except, [:id]}, conflict_target: [:link])
    end
  end

  # ...
end

That should work fine for new entries where I pass an attrs map. But how about an existing %Midia{}? How can I pass it to the changeset without lose not casted field like id primary key?

I’ts not possible to call changeset/3 as

Midia.changeset(%Midia{id: "existing"}, %Midia{id: "existing", link: "new link"})

Does it make sense?

This works, but is it semantic correct or there’s a better way to do this?

  @impl true
  def upsert(midia \\ %Midia{}, attrs) do
    attrs = (is_struct(attrs) && Map.from_struct(attrs)) || attrs
    tags = attrs[:tags] || midia.tags

    with {:ok, midia} <- Midia.changeset(midia, attrs, tags) do
      Repo.insert(midia, on_conflict: {:replace_all_except, [:id]}, conflict_target: [:link])
    end
  end

Oh, if is a insert I only pass new attrs, which is already a map, and if is an update the first arg will be the existing and attrs would be a map with new data.

I’m feeling kinda dumb right now for this simple doubt…