Ecto.NoPrimaryKeyValueError when using Multi.insert_or_update

I’m running into an Ecto.NoPrimaryKeyValueError when using Multi.insert_or_update.

The function below receives a JSON payload for a Reference record. Embedded is a products attribute which contains a list of products that belong to the given reference. The reference may or may not exist. If the reference exists, its products may or may not exist. A product can not exist without a reference.

A Product belongs to a Reference and a Store. Here’s an example payload, simplified for brevity:

%{
  "brand_name" => "Philips",
  "ean" => "707",
  "sku" => "123",
  "name" => "Screw Driver",
  "products" => [%{"store_id" => 1}],
  "weight_in_grams" => 1000
}

The following works only if the Product already exists. If the product is new, I get an Ecto.NoPrimaryKeyValueError. I’m not sure if this is a bug, or if I’m doing something wrong.

def create_or_update_reference(sku, %{"products" => product_params} = reference_params) do
  changeset =
    case Catalog.get_reference_by_sku(sku) do
      nil ->
        %Reference{}
      reference ->
        reference
    end
    |> Reference.changeset(reference_params)

  Multi.new()
  |> Multi.insert_or_update(:reference, changeset)
  |> Multi.merge(fn %{reference: reference} ->
    insert_or_update_products(reference, product_params)
  end)
  |> run_multi()
end

defp insert_or_update_products(%Reference{id: reference_id} = reference, product_params) do
  product_params
  |> Enum.map(fn %{"store_id" => store_id} = params ->
    store_id
    |> Stores.get_product_by_store_and_reference(reference_id)
    |> case do
      nil ->
        %Product{store_id: store_id, reference_id: reference_id}
      product ->
        product
    end
    |> Product.changeset(params)
  end)
  |> Enum.map(fn changeset ->
    operation = String.to_atom("product_#{unique_integer}")
    Multi.update(Multi.new(), operation, changeset)
  end)
  |> Enum.reduce(Multi.new(), &Multi.append/2)
end

defp run_multi(multi) do
  case Repo.transaction(multi) do
    {:ok, result} ->
      {:ok, result}
    {:error, _failed_op, changeset, _changes} ->
      {:error, changeset}
  end
end

defp unique_integer, do: System.unique_integer([:positive])

Here’s the full error:

test create_or_update_reference/2 with valid data create_or_update_reference/2 when reference does not exist (Obramax.ReferenceRegistrarTest)
     test/obramax/reference_registrar/reference_registrar_test.exs:8
     ** (Ecto.NoPrimaryKeyValueError) struct `%Obramax.Stores.Product{__meta__: #Ecto.Schema.Metadata<:built, "products">, id: nil, inserted_at: nil, order_management_code: nil, price: #Ecto.Association.NotLoaded<association :price is not loaded>, reference: #Ecto.Association.NotLoaded<association :reference is not loaded>, reference_id: 11, store: #Ecto.Association.NotLoaded<association :store is not loaded>, store_id: 10, updated_at: nil}` is missing primary key value
     code: assert {:ok, %Reference{} = reference} = ReferenceRegistrar.create_or_update_reference(reference_params["sku"], reference_params)
     stacktrace:
       (ecto) lib/ecto/repo/schema.ex:763: anonymous fn/3 in Ecto.Repo.Schema.add_pk_filter!/2
       (elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3
       (ecto) lib/ecto/repo/schema.ex:257: Ecto.Repo.Schema.do_update/4
       (ecto) lib/ecto/multi.ex:421: Ecto.Multi.apply_operation/5
       (elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3
       (ecto) lib/ecto/multi.ex:411: anonymous fn/5 in Ecto.Multi.apply_operations/5
       (db_connection) lib/db_connection.ex:1374: DBConnection.transaction_nested/2
       (db_connection) lib/db_connection.ex:1234: DBConnection.transaction_meter/3
       (db_connection) lib/db_connection.ex:798: DBConnection.transaction/3
       (ecto) lib/ecto/multi.ex:447: Ecto.Multi.apply_operation/4
       (ecto) lib/ecto/multi.ex:421: Ecto.Multi.apply_operation/5
       (elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3
       (ecto) lib/ecto/multi.ex:411: anonymous fn/5 in Ecto.Multi.apply_operations/5
       (ecto) lib/ecto/adapters/sql.ex:576: anonymous fn/3 in Ecto.Adapters.SQL.do_transaction/3
       (db_connection) lib/db_connection.ex:1283: DBConnection.transaction_run/4
       (db_connection) lib/db_connection.ex:1207: DBConnection.run_begin/3
       (db_connection) lib/db_connection.ex:798: DBConnection.transaction/3
       (ecto) lib/ecto/repo/queryable.ex:23: Ecto.Repo.Queryable.transaction/4
       (obramax) lib/obramax/reference_registrar/reference_registrar.ex:77: Obramax.ReferenceRegistrar.run_multi/1
       test/obramax/reference_registrar/reference_registrar_test.exs:43: (test)

Figure it out. I was calling update instead of insert_or_upate on the nested records. Duh!

Multi.update(Multi.new(), operation, changeset)

Should be

Multi.insert_or_update(Multi.new(), operation, changeset)
1 Like