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!