You didn’t put your code so here’s an example which you can easily refactor to use in your own code. This function is from projects.ex file and not from project.ex which contains the schema.
def create_project(attrs, user) do
%Project{}
|> Project.changeset(attrs)
|> Changeset.put_assoc(:user, user)
|> Repo.insert()
end
This function creates a project, and to update it, you just have to create update function without put_assoc.
def update_project(attrs, user) do
%Project{}
|> Project.changeset(attrs)
|> Repo.update()
end
Single Ecto.changeset.t/1 type defines changes as a public key we can simply use it as a normal Map which means Map.has_key?/2 would work without any problem.
Here is a minimal working example:
Mix.install([:ecto])
defmodule Blog do
defmodule Post do
use Ecto.Schema
alias Ecto.Changeset
schema "posts" do
field(:author, :string)
field(:content, :string)
field(:title, :string)
has_many(:comments, Blog.Comment)
end
def changeset(post, attrs) do
Changeset.cast(post, attrs, ~w[author content title]a)
end
end
defmodule Comment do
use Ecto.Schema
alias Ecto.Changeset
schema "comments" do
belongs_to(:post, Blog.Post)
field(:author, :string)
field(:content, :string)
end
def changeset(comment, attrs) do
comment
|> Changeset.cast(attrs, ~w[author content]a)
|> put_new_assoc(:post, attrs[:post])
end
def put_new_assoc(%Changeset{changes: changes} = changeset, key, value) do
if Map.has_key?(changes, key) do
changeset
else
Changeset.put_assoc(changeset, key, value)
end
end
end
end
defmodule Example do
alias Blog.{Comment, Post}
alias Ecto.Changeset
def sample do
post =
%Post{}
|> Post.changeset(%{author: "Foo", content: "Foo's content", title: "Foo's title"})
|> Changeset.apply_changes()
comment =
%Comment{}
|> Comment.changeset(%{author: "Bar", content: "Bar's content", post: post})
|> Changeset.apply_changes()
updated_post =
post
|> Post.changeset(%{content: "Updated Foo's content"})
|> Changeset.apply_changes()
updated_comment =
%Comment{}
|> Comment.changeset(%{author: "Bar", content: "Bar's content", post: post})
|> Comment.changeset(%{post: updated_post})
|> Changeset.apply_changes()
comment.post.content == updated_comment.post.content
end
end
iex> Example.sample()
# true
If you only want the put_assoc to happen when making an Ecto.Changeset for a new record, consider splitting your single changeset function into specific functions for create vs update.
Thanks for everybody’s response I think this is the best function so far
def put_assoc_new(changeset, key, key_id, value) do
if changeset |> get_field(key_id) |> is_nil do
put_assoc(changeset, key, value)
else
changeset
end
end