Vladimir E
Vladimir E

Reputation: 47

How to make Ecto.Changeset.cast_assoc to update associated records instead of delete + insert

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

Answers (1)

Vladimir E
Vladimir E

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

Related Questions