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
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
Hi everyone,
I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding.
I sta...
New
Hello,
I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
Documentation
While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
So my question is quite simple and i have found no conclusive answer on forum, google or AI.
Should we use :erlang.float for Integer to ...
New
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted”
Version...
New
Other Trending Topics
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
Hi there! We created Gust: A task orchestrator inspired by Airflow.
For those who have never heard about Aiflow, it’s a Python-based wor...
New
Hi everyone!
The first release candidate for the Expert language server project is now available!
We’ve published a press release detai...
New
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
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
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #elixirconf-eu
- #metaprogramming
- #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?