dfalling

dfalling

How to manage associations during creates/updates?

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.

  1. Load Photos from changeset, to ensure they all exist and belong to the user (privileges)
  2. Create or update Element in an Ecto.Multi
  3. 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!

Most Liked

moogle19

moogle19

Yes, validate_required for the foreign_keys doesn’t work with cast_assoc.

Take a look at the documentation.
You can specify required: true as an options in cast_assoc.

Where Next?

Popular in Questions Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&query=perfume&page=2, I would like to get: ...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30877 112
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 126479 1222
New

We're in Beta

About us Mission Statement