如何使Ecto.Changeset.cast_assoc更新相关记录而不是删除+插入

时间:2018-04-26 17:46:10

标签: elixir ecto

我有以下情况:

我将产品及其变体(has_many)存储在数据库中。然后我在一个请求中将Product及其Variants发送到API:

payload = %Product{
  # ...
  variants: [
    %Variant{
      # ...
      option1: "Black",
      option2: "64GB",
    },
    %Variant{
      # ...
      option1: "Silver",
      option2: "64GB",
    }
  ],
  vendor: "Apple"
}

当我得到回复时,我需要在我的数据库中更新产品及其变体。响应:

%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",
    }
  ],
}

当我尝试使用cast_assoc更新关联的变体时,ecto会删除我在数据库中的变体记录,并从有效负载中插入相同的变体记录。

我知道那是因为on_replace: :delete,但如果我没有设置:on_replace,Ecto会抱怨它不知道如何替换。但为什么要替换?

我需要更新相关记录。我尝试将数据库中的变量id注入到响应中,我将其用作更新的属性,因此Ecto可以将响应中的变体与数据库中的变体链接起来。

全天与此战斗。甚至可以更新相关记录吗?我错过了什么?

这是我的架构:

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

应该完成工作的简化代码:

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
>

1 个答案:

答案 0 :(得分:0)

原来cast_assoc在这里:

|> cast_assoc(:variants, with: &Variant.changeset_from_response/2)

将现有变体与属性变体匹配,以便能够调用Variant.changeset_from_response(variant, attrs)

如果您在id中没有正确的变体attrs,则会认为attrs用于创建新记录。

我在id内向attrs添加了Variant.changeset_from_response/2,为时已晚。