dfalling
I have a schema Element with a has_many relationship to Photo. I want to manage this relationship when creating/updating an Element. I persist the order of the Photos in an order field in the join table. Right now I do all this manually with a lot more code than I’d like.
- Load Photos from changeset, to ensure they all exist and belong to the user (privileges)
- Create or update Element in an Ecto.Multi
- Manually create the ElementPhoto schemas after comparing with existing ElementPhotos
Is there a cleaner way to do this? I looked into Ecto.Changeset.put_assoc, but it 1) doesn’t generate IDs for my join table (I use UUIDs), and 2) doesn’t seem to provide a way to update the order field.
def create_user_element(attrs, user_id) do
changeset =
%Element{user_id: user_id}
|> Element.changeset(attrs)
|> validate_element_photos(user_id)
Multi.new()
|> Multi.insert(:element, changeset)
|> Multi.merge(fn %{element: element} ->
new_photo_ids = Ecto.Changeset.get_field(changeset, :photo_ids)
element_photos_multi(element.id, [], new_photo_ids)
end)
|> Repo.transaction()
|> case do
{:ok, %{element: element}} -> {:ok, element}
{:error, _field, %Ecto.Changeset{} = changeset, _} -> {:error, changeset}
end
end
defp validate_element_photos(changeset, user_id) do
photo_ids = Ecto.Changeset.get_field(changeset, :photo_ids, [])
photos =
Photo
|> where([photo], photo.id in ^photo_ids and photo.user_id == ^user_id)
|> Repo.all()
found_ids = Enum.map(photos, & &1.id)
missing_ids = photo_ids -- found_ids
case missing_ids do
[] ->
changeset
missing_ids ->
Enum.reduce(missing_ids, changeset, fn missing_id, changeset ->
Ecto.Changeset.add_error(
changeset,
:photo_ids,
"Photo #{missing_id} not found"
)
end)
end
end
defp element_photos_multi(element_id, existing_photo_ids, new_photo_ids) do
removed_photo_ids = existing_photo_ids -- new_photo_ids
added_photo_ids = new_photo_ids -- existing_photo_ids
multi = Multi.new()
case {removed_photo_ids, added_photo_ids} do
{[], []} ->
# no-op, no changes
multi
_ ->
# delete all previous items (easier than trying to rearrange them)
multi =
multi
|> Multi.delete_all(
:delete_photos,
from(p in ElementPhoto,
where: p.element_id == ^element_id
)
)
# add all items in proper order
new_photo_ids
|> Enum.with_index()
|> Enum.reduce(multi, fn {photo_id, index}, multi ->
changeset =
%ElementPhoto{}
|> ElementPhoto.changeset(%{element_id: element_id, photo_id: photo_id, order: index})
multi
|> Multi.insert("photo #{Integer.to_string(index)}", changeset)
end)
end
end
Thanks!
Trending in Questions
Hello!
Suppose you are building workflow (order / task / payment) processing system with the following requirements:
Each workflow con...
New
I’m aware of 2 such projects, are there others?
git: GitHub - team-alembic/realworld: A fullstack Phoenix LiveView application with back...
New
Hi all,
I’m working on a Phoenix project where I need to make external HTTP requests and I’m trying to choose between Req and HTTPoison....
New
Hi everyone.
My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
Hi,
we run different LLM models across a cluster, with each node loading a configurable subset. Model-specific queues let nodes consume ...
New
Hi all, I’m new here and would like to hear peoples views on my Elixir/Phoenix based game-engine! :smiley:
Mutonex gamedesign was finish...
New
Do we have maintainer of absinthe-graphql/dataloader here? We have a PR that we’d like to merge to the library. Relatively small additive...
New
Other Trending Topics
Hey there,
I haven’t seen a list of useful Elixir specific skills for the popular AI tools yet so I’m starting one here with my first.
...
New
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself.
My main conc...
New
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly)
AI dream has many faces, but general...
New
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #ai
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #blog-post
- #ecto-query
- #elixirconf-us
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #elixirconf-eu
- #api
- #forms
- #security
- #hex










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
moogle19
You could write the assocs in your schema like this:
and in your
changeset/2addcast_assoc(:element_photos)(also addcast_assocforphototo yourElementPhoto).Then you have to give the nested changes to your changeset function like so:
Depending on what should happend on update or delete you should also set
on_replaceandon_deletefor the associations.dfalling
Thanks, I think that’s gotten me really close!
My changes are failing though because there’s no
:element_idin the ElementPhoto. Do I have to add that manually? This would break the process of using this in a create Changeset, since I don’t have the Element’s id yet.moogle19
If you are using
cast_assoc/2theelement_idshould get set automatically during theinsert/update.dfalling
Hrm, strange. I’m doing that.
My PhotoElement has
cast_assoc(:photo)andcast_assoc(:element), as well asbelongs_tofor both of those associations. My PhotoElement changeset does havevalidate_required([:element_id, :trip_id])…so maybe I have to validate eitherelement_idORelement? But that error above doesn’t show the element in the changes, so it appears it wasn’t included either way.moogle19
Yes,
validate_requiredfor the foreign_keys doesn’t work withcast_assoc.Take a look at the documentation.
You can specify
required: trueas an options incast_assoc.dfalling
Ok, I replaced
validate_requiredwith my ownvalidate_eitherfunction which ensures either the ID or association is present. The changeset is still failing because neither theelementandelement_idare present. Ecto doesn’t seem to be making that connection. Is there a step I missed to ensure it fills in that property?moogle19
Could you share your schemes / changeset functions?
dfalling
Sure! Here are my updated data functions (I’m trying to add Trip and Photo associations):
Here’s the attrs that goes into the changeset function:
And the resulting changeset error:
moogle19
Yeah, that doesn’t work. The
ElementPhotochangeset doesn’t have anElementorelement_idwhen you create it viacast_assoc.If you plan to create an
ElementPhotooutside of theElementcontext I would suggest you to create a new changeset function without theElementvalidation and use that one with thehas_manyassociation, otherwise just remove the validation completly.dfalling
Yeah, exactly I was trying to understand how that could work. Since this is a changeset to create the Element, I don’t have it yet to put the ID in the association. That’s why I was using an
Ecto.Multibefore so I could get the element to use it’s ID for the children. So I guess there isn’t a cleaner way with Ecto to avoid this multi-step process I’m currently doing?