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

Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New

Other popular topics Top

KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36654 110
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New

We're in Beta

About us Mission Statement