dev.immranderson

dev.immranderson

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

Hey! My apologies if this question is a bit confusingly worded, I’m a beginner to phoenix and am picking up the lingo along the way.

I’m building a phoenix api project and I have been having a bit of trouble wrapping my head around how to implement building associations between 2 schemas for when you might not have previously inserted them in the past – but still have the flexibility available to me to add a child object into its own schema.

More concretely, I have the concept of a seller and a product. I’d like to be able to insert products on their own into my db, but I also want to be able to create a new seller with a new product and insert it into the db in one transaction. If the product actually happened to already exist when inserting a new merchant, I’d like to have that product then be associated with the merchant.

So, from a client, I was hoping that my inbound json payload would potentially be represented by the following structure.

Note, at this point, neither the seller or the product exists in the db yet:

{
	"seller": {
		"name": "seller1",
		"description": "the first seller",
		"product": {
			"name": "product1"
		}
	}
}

After this point, I would like to make it so I could proceed to do something where I can create a new seller but link it up with a potentially existing product that already exists in my products table:

{
	"seller": {
		"name": "seller2",
		"description": "the second seller",
		"product": {
			"name": "product1"
		}
	}
}

I’m actually struggling with the first step, because I’m not entirely sure how to insert both of those new entities at the same time and build out the association upon insert. Do I need to insert them one at a time? Or is it at all possible for me to do it all in one transaction? Or do I need to somehow access the child product json object, insert that into my products db, then after it has been inserted, proceed to insert the seller. After the seller is inserted, proceed to build the association? I feel like there should be a way to insert them both at the same time?

What I have so far:

My Seller Schema:

defmodule MyApp.Sellers.Seller do
  use Ecto.Schema
  import Ecto.Changeset
  @primary_key {:id, :binary_id, autogenerate: true}
  schema "sellers" do
    field :name, :string
    field :description, :string

    many_to_many :products, MyApp.Offerings.Product,
      join_through: "sellers_products",
      on_replace: :delete

    timestamps()
  end

  def changeset(seller, attrs) do
    seller
    |> cast(attrs, [
      :name,
      :description,
    ])
    |> cast_assoc(:products) #How do I create both a seller and a product at the same time from the nested json?
    |> validate_required([:name, :description])
  end
end

My Product Schema:

defmodule MyApp.Offerings.Product do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  schema "products" do
    field :name, :string

    many_to_many :sellers, MyApp.Sellers.Seller,
      join_through: "sellers_products",
      on_replace: :delete

    timestamps()
  end

  def changeset(product, attrs) do
    product
    |> cast(attrs, [
      :name
    ])
    |> validate_required([
      :name
    ])
  end
end

My associated migrations:

defmodule MyApp.Repo.Migrations.CreateProducts do
  use Ecto.Migration

  def change do
    create table(:products, primary_key: false) do
      add :id, :uuid, primary_key: true
      add :name, :string, null: false
      timestamps()
    end

    create unique_index(:products, [:name]) #Will help guarantee uniqueness
  end
end
defmodule MyApp.Repo.Migrations.CreateSellers do
  use Ecto.Migration

  def change do
    create table(:sellers, primary_key: false) do
      add :id, :uuid, primary_key: true
      add :name, :string, null: false
      add :description, :text, null: false

      timestamps()
    end
  end
end

And the many_to_many table creation:

defmodule MyApp.Repo.Migrations.AssociateProductsToSellers do
  use Ecto.Migration

  def change do
    create table(:sellers_products, primary_key: false) do
      add :seller_id, references(:sellers, type: :uuid), primary_key: true
      add :product_id, references(:products, type: :uuid), primary_key: true
      timestamps()
    end

    create index(:sellers_products, [:seller_id])
    create index(:sellers_products, [:product_id])

    create unique_index(:sellers_products, [:seller_id, :product_id])
  end
end

And finally, my Sellers Context create function:

  def create_seller(attrs \\ %{}) do #Should I be inserting the product here first, then inserting a seller, then associating them??
    |> Seller.changeset(attrs)
    |> Repo.insert()
  end

Marked As Solved

Lucassifoni

Lucassifoni

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..

Last Post!

Lucassifoni

Lucassifoni

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)

Where Next?

Popular in Questions Top

joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
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
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

Other popular topics Top

JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 44139 214
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

We're in Beta

About us Mission Statement