Reputation: 47
I have the following scenario:
I store product and its variants (has_many
) in the database. Then I send Product and its Variants to API in one request like that:
payload = %Product{
# ...
variants: [
%Variant{
# ...
option1: "Black",
option2: "64GB",
},
%Variant{
# ...
option1: "Silver",
option2: "64GB",
}
],
vendor: "Apple"
}
When I get response I need to update product and its variants in my database. Response:
%Shopify.Product{
# ...
created_at: "2018-04-26T12:54:33-04:00",
variants: [
%Shopify.Variant{
# ...
created_at: "2018-04-26T12:54:33-04:00",
option1: "Black",
option2: "64GB",
title: "Black / 64GB",
},
%Shopify.Variant{
# ...
created_at: "2018-04-26T12:54:33-04:00",
option1: "Silver",
option2: "64GB",
title: "Silver / 64GB",
}
],
}
When I try to update associated variants using cast_assoc
, ecto deletes variant records that I have in database and inserts new ones from payload which are the same.
I know that's because on_replace: :delete
, but if I don't set :on_replace
, Ecto will complain that it doesn't know what to do on replace. But why replace?
I need to update associated records. I tried injecting variant ids from database into the response, which I use as attributes for update, so Ecto can link variants from response with those I have in the database.
Fighting with this all the day. Is it even possible to update associated records? What am I missing?
Here's my schema:
defmodule Product do
# ...
has_many(:variants, Variant, on_replace: :delete)
# ...
def changeset_from_response(product, response) do
attrs =
response
|> Utils.map_from_struct_deep()
|> put_variant_ids(product)
product
|> cast(attrs, @castable_attrs)
# ...
# HERE **********
|> cast_assoc(:variants, with: &Variant.changeset_from_response/2)
# HERE **********
end
end
defmodule Variant do
# ...
belongs_to(:product, Product)
# ...
def changeset_from_response(variant, response) do
attrs =
response
|> Utils.map_from_struct_deep()
variant
|> cast(attrs, @castable_attrs ++ [:id, :product_id])
# ...
end
end
And simplified code that should do the work:
product = Repo.get(Product, id) |> Repo.preload(:variants)
payload = # product to payload
shopify_product = Shopify.Product.create(payload)
changeset = Product.changeset_from_response(product, shopify_product)
iex> changeset
#Ecto.Changeset<
action: nil,
changes: %{
# ...
shopify_created_at: ~N[2018-04-26 12:54:33],
shopify_id: "1333797453908",
variants: [
#Ecto.Changeset<action: :replace, changes: %{}, errors: [],
data: #ProdSync.Data.Variant<>, valid?: true>,
#Ecto.Changeset<action: :replace, changes: %{}, errors: [],
data: #ProdSync.Data.Variant<>, valid?: true>,
***** HERE IT DELETES VARIANTS AND INSERTS NEW ONES ********
#Ecto.Changeset<
action: :insert,
changes: %{
id: 26,
option1: "Black",
option2: "64GB",
title: "Black / 64GB",
},
errors: [],
data: #ProdSync.Data.Variant<>,
valid?: true
>,
#Ecto.Changeset<
action: :insert,
changes: %{
id: 27,
option1: "Silver",
option2: "64GB",
title: "Silver / 64GB",
},
errors: [],
data: #ProdSync.Data.Variant<>,
valid?: true
>,
]
},
errors: [],
data: #ProdSync.Data.Product<>,
valid?: true
>
Upvotes: 0
Views: 2163
Reputation: 47
Turns out that cast_assoc
here:
|> cast_assoc(:variants, with: &Variant.changeset_from_response/2)
matches existing variants with variants from attributes to be able to call Variant.changeset_from_response(variant, attrs)
and if you don't have proper variant id
in attrs
it will consider that attrs
is for creating new record.
I was adding id
to attrs
inside Variant.changeset_from_response/2
which is too late.
Upvotes: 1