Как заставить Ecto.Changeset.cast_assoc обновлять связанные записи вместо удаления + вставки - PullRequest
0 голосов
/ 26 апреля 2018

У меня есть следующий сценарий:

Я храню товар и его варианты (has_many) в базе данных. Затем я отправляю Product и его варианты в 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 может связать варианты из ответа с теми, что есть в базе данных.

Борьба с этим весь день. Можно ли вообще обновить связанные записи? Чего мне не хватает?

Вот моя схема:

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 голосов
/ 27 апреля 2018

Оказывается, что 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, что слишком поздно.

...