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
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
Popular in Questions
Other popular topics
Latest Phoenix Threads
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #hex
- #security









