1
votes

I receive an error on an Association when creating a child record when passing the ID values of the parent records of a 1 to Many association.

The error says I need to preload User which is a parent record to Transaction,

  1) test creates and renders resource when data is valid (MyRewardsWeb.TransactionControllerTest)
       test/controllers/transaction_controller_test.exs:36
       ** (RuntimeError) attempting to cast or change association `user` from `MyRewards.Transaction` that was not loaded. Please preload your associations before manipulating them thr
  ough changesets
       stacktrace:
         (ecto) lib/ecto/changeset/relation.ex:66: Ecto.Changeset.Relation.load!/2
         (ecto) lib/ecto/repo/schema.ex:514: anonymous fn/4 in Ecto.Repo.Schema.surface_changes/4

         (elixir) lib/enum.ex:1623: Enum."-reduce/3-lists^foldl/2-0-"/3
         (ecto) lib/ecto/repo/schema.ex:503: Ecto.Repo.Schema.surface_changes/4
         (ecto) lib/ecto/repo/schema.ex:186: Ecto.Repo.Schema.do_insert/4
         (my_rewards_web) web/controllers/transaction_controller.ex:20: MyRewardsWeb.TransactionController.create/2
         (my_rewards_web) web/controllers/transaction_controller.ex:1: MyRewardsWeb.TransactionController.action/2
         (my_rewards_web) web/controllers/transaction_controller.ex:1: MyRewardsWeb.TransactionController.phoenix_controller_pipeline/2
         (my_rewards_web) lib/my_rewards_web/endpoint.ex:1: MyRewardsWeb.Endpoint.instrument/4
         (my_rewards_web) lib/phoenix/router.ex:261: MyRewardsWeb.Router.dispatch/2
         (my_rewards_web) web/router.ex:1: MyRewardsWeb.Router.do_call/2
         (my_rewards_web) lib/my_rewards_web/endpoint.ex:1: MyRewardsWeb.Endpoint.phoenix_pipeline/1
         (my_rewards_web) lib/my_rewards_web/endpoint.ex:1: MyRewardsWeb.Endpoint.call/2
         (phoenix) lib/phoenix/test/conn_test.ex:224: Phoenix.ConnTest.dispatch/5
         test/controllers/transaction_controller_test.exs:41: (test)

In my controller method, I find User and Merchant records which are both parent records of the Transaction struct and pass the respective record ID's to when creating the changeset

  def create(conn, %{"merchant_id" => merchant_id, "mobile" => mobile, "amount" => amount}) do
    customer = MyRewards.find_user!(%{"mobile" => mobile})
    merchant = Repo.get(MyRewards.Merchant, merchant_id)
    changeset = MyRewards.create_deposit_changeset(%{"user_id" => customer.id, "merchant_id" => merchant.id, "amount" => amount })
    case Repo.insert(changeset) do
      {:ok, transaction} ->
        conn
        |> put_status(:created)
        |> put_resp_header("location", transaction_path(conn, :show, transaction))
        |> render("show.json", transaction: transaction)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(MyRewardsWeb.ChangesetView, "error.json", changeset: changeset)
    end
  end

This function just passes the info along and returns the changeset:

 def create_deposit_changeset(user_id: user_id, merchant_id: merchant_id, amount: amount) do
   MyRewards.Transaction.deposit(%MyRewards.Transaction{}, %{ merchant_id: merchant_id, user_id: user_id, amount: Money.new(amount) })
 end

In the struct, I have the relationships defined and cast the user_id and merchant_id correctly as parameters in the Transaction Ecto Struct definition.

defmodule MyRewards.Transaction do

  @entry_types [:credit, :debit]

  use MyRewards.Model
  schema "my_rewards_transactions" do
    field :amount, Money.Ecto.Type
    field :type, :string
    belongs_to :user, MyRewards.User
    belongs_to :merchant, MyRewards.Merchant

    timestamps()
  end

  @doc """
  Builds a changeset based on the `struct` and `params`.
  """
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:amount, :type, :user_id, :merchant_id])
    |> validate_required([:amount, :type])
  end

  def deposit(struct, params) do
    struct
    |> cast( %{type:  "debit"}, [:type])
    |> changeset(params)
  end


  def widthdraw(struct, params) do
    struct
    |> cast( %{type: "credit"}, [:type])
    |> changeset(params)
  end
end

When I test the function in my test cases, it enters the transaction fine.

MyRewards.create_deposit_changeset(user_id: customer.id, merchant_id: merchant.id, amount: x) 

But then when I run it in the Phoenix Controller, it fails. I'm not sure why it fails or asks for the preloading of the parent association. Should I be doing a put_assoc somewhere instead of casting user_id and merchant_id directly as parameters?

1
only the test fails?Igor Belo
The test passes but the controller action failsDogEatDog
check this line: changeset = MyRewards.create_deposit_changeset(%{"user_id" => customer.id, "merchant_id" => merchant.id, "amount" => amount })...from your scenario there's no function clause matching, is it correct?Igor Belo
that was not the issue, but thanks. I had overloaded the function call to accept both types of parametersDogEatDog

1 Answers

2
votes

The issue is that my setup as an OTP application can not share the preloaded aspect of Ecto. My web side in Phoenix is fine but I cannot commit the changeset in the controller because the Repo in the Phoenix app is not the same reference as the changeset that is OTP app, which is causing the preload error.

The reason I had extracted it was due to a ! i had missed on my original setup.

I changed my controller back to look like this where it returns {:ok, transaction} like this

case MyRewards.create_deposit(%{"user_id" => customer.id, "merchant_id" => merchant.id, "amount" => amount }) do
  {:ok, transaction} ->
    conn
    |> put_status(:created)
    |> put_resp_header("location", transaction_path(conn, :show, transaction))
    |> render("show.json", transaction: transaction)
  {:error, changeset} ->
    conn
    |> put_status(:unprocessable_entity)
    |> render(MyRewardsWeb.ChangesetView, "error.json", changeset: changeset)
end

This is different because I'm not returning the changeset. I'm performing the Repo.insert in the OTP app and returning the transaction.

def create_deposit(%{"user_id" => user_id, "merchant_id" => merchant_id, "amount" => amount }) do
  MyRewards.Transaction.deposit(%MyRewards.Transaction{}, %{ merchant_id: merchant_id, user_id: user_id, amount: Money.new(amount) })
    |> MyRewards.Repo.insert
end

The original reason why I had changed from this was because I was using |> MyRewards.Repo.insert! instead of |> MyRewards.Repo.insert.... that ! bang at the end changes the return of the function to only return the struct instead of returning a status and a struct like {:ok, transaction} which is what the controller is expecting.

In short, if you have your Application split into OTP and a Phoenix, watch out for ! in your repo inserts.