Trying to figure out how to create or update multiple many_to_many items and associations when none of the items don't exist in the db yet

Hello,
Here’s a playground project where I’m setting a project and its associations upon creation :

schema "projects" do
    field :date_end, :date
    field :date_start, :date
    field :description, :string
    field :display_date, :string
    field :draft, :boolean, default: false
    field :place, :string
    field :price, :integer
    field :text, :string
    field :title, :string
    field :url, :string
    many_to_many :tags, Docs.Contents.Tag, join_through: "projects_tags"
    many_to_many :images, Docs.Contents.Photo, join_through: "projects_photos"
    has_many :translations, Docs.Contents.Translation, foreign_key: :target_id, where: [table: "projects"]
    belongs_to :commanditaire, Docs.Contents.Commanditaire
    belongs_to :main_image, Docs.Contents.Photo
    belongs_to :category, Docs.Contents.Category
    has_many :metadata, Docs.Contents.Metadata
    many_to_many :blogs, Docs.Contents.Blog, join_through: "blogs_projects"
    timestamps()
  end

def changeset(
        project,
        %{
          "tags" => t,
          "commanditaire" => co,
          "category" => ca,
          "metadata" => m,
          "images" => i,
          "main_image" => mi,
          "translations" => tr
        } = attrs
      ) do
    tags = Docs.Contents.DataUtilitites.get_or_create_tags(t)
    commanditaire = Docs.Contents.DataUtilitites.get_or_create_commanditaire(co)
    category = Docs.Contents.DataUtilitites.get_or_create_category(ca)
    metadata = to_metadata(m)
    translations = Docs.Contents.DataUtilitites.create_or_update_translations(tr, "projects", project)
    images = to_images(i)
    main_image = to_main_image(mi)
    project
    |> cast(attrs, [
      :title,
      :draft,
      :url,
      :description,
      :date_start,
      :date_end,
      :display_date,
      :text,
      :price,
      :place,
    ])
    |> put_assoc(:tags, tags)
    |> put_assoc(:main_image, main_image)
    |> put_assoc(:category, category)
    |> put_assoc(:commanditaire, commanditaire)
    |> put_assoc(:images, images)
    |> put_assoc(:metadata, metadata)
    |> put_assoc(:translations, translations)
    |> validate_required([
      :title,
      :draft,
      :url,
      :description,
      :date_start,
      :date_end,
      :display_date,
      :price,
      :place,
      :text
    ])
  end

And the assoc fetch-or-create part :

defmodule Docs.Contents.DataUtilitites do
  @moduledoc """
  Contains helpers that are generic enough to serve
  all of our CRUD models.
  """

  def get_or_create_category(category) do
    if is_binary(category) do
      {:ok, c} = Docs.Contents.create_category(%{"name" => category, "slug" => Slug.slugify(category)})
      c
    else
      Docs.Contents.get_category!(Map.get(category, "id"))
    end
  end

  def get_or_create_tags(tags) do
    {existing_tags, new_tags} = tags
      |> Enum.reduce({[],[]}, fn tag, {ex, new} ->
          if is_binary(tag) do
            {:ok, new_tag} = Docs.Contents.create_tag(%{"name" => tag, "slug" => Slug.slugify(tag)})
            {ex, [new_tag | new]}
          else
            db_tag = Docs.Contents.get_tag!(Map.get(tag, "id"))
            {[db_tag | ex], new}
          end
      end)
    existing_tags ++ new_tags
  end

  def create_or_update_translations(translations, table, obj \\ nil) do
    Enum.map(translations, fn trans ->
      if Map.has_key?(trans, "id") do
        db_trans = Docs.Contents.get_translation!(Map.get(trans, "id"))
        {:ok, db_trans} = db_trans |> Docs.Contents.update_translation(%{
          "content" => Map.get(trans, "content")
        })
        db_trans
      else
        {:ok, db_trans} = Docs.Contents.create_translation(%{
          "content" => Map.get(trans, "content"),
          "table" => table,
          "lang" => Map.get(trans, "lang"),
          "target_id" => obj.id
        })
        db_trans
      end
    end)
  end

  def get_or_create_commanditaire(commanditaire) do
    if is_binary(commanditaire) do
      {:ok, c} = Docs.Contents.create_commanditaire(%{"name" => commanditaire, "slug" => Slug.slugify(commanditaire)})
      c
    else
      Docs.Contents.get_commanditaire!(Map.get(commanditaire, "id"))
    end
  end

Some parts are redundant with what Ecto is capable of doing for you, or not-too-clean. It’s a test. You can find the whole thing here if you’d like : https://github.com/Lucassifoni/documents-next/tree/master/lib/docs/contents

Hoping it would be useful to you. Don’t pay too much attention to translation/form generation, it’s tangent to your question…