Assume an Author
schema:
schema "authors" do
field :name, :string
has_many :posts, Post
end
def changeset(struct, attrs) do
struct
|> cast(attrs, [:name])
end
I can perform an update:
author |> Author.changeset(%{name: "Jo"}) |> Blog.Repo.update!()
but an upsert fails:
author |> Author.changeset(%{name: "Jo"}) |> Blog.Repo.insert!(on_conflict: {:replace_all_except, [:id]})
** (RuntimeError) attempting to cast or change association `posts` from `Author` that was not loaded. Please preload your associations before manipulating them through changesets
Is it possible to perform an upsert without loading the associations?
Or is this by design?
Have you taken a look at Repo.insert_all/3
? They have a full section on upsert , maybe that helps.
Something like
query = Author |> where([a], a.id == ^id) |> select([a], %{name: "Jo"})
Repo.insert_all(Author, query, on_conflict: : replace_all)
Maybe Repo.insert_or_update
is what you’re looking for. A changeset is already aware if you loaded author
from the db or not.
The code that implements replace_all_except
starts from the schema’s list of fields:
defp replace_all_fields!(_kind, schema, to_remove) do
Enum.map(schema.__schema__(:fields) -- to_remove, &field_source!(schema, &1))
end
And that’s a list of built from all the field
calls inside the schema
block - associations aren’t in there.
Can you post the stacktrace of the error and/or the full source of Author.changeset/2
?
Thanks for all the feedback. Along with a discussion on the Elxir discord I’ve improved my understanding of how upsert works.
What I should be doing is this:
%Author{} |> Author.changeset(%{name: "Jo"}) |> Blog.Repo.insert!(on_conflict: {:replace_all_except, [:id]})
If the struct metadata state is built
then it works fine. If it’s loaded
then the associations are expected.
I want to avoid loading it first, and just insert or update an existing record, so using a built struct is the best approach.
If I had the struct already loaded I can use insert_or_update