Ecto - 在使用父关联创建记录时预加载关联

Ecto - Preload association when creating record with parent associations

我在传递一对多关联的父记录的 ID 值时创建子记录时收到关联错误。

错误说我需要预加载 User,它是 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)

在我的控制器方法中,我找到 UserMerchant 记录,它们都是 Transaction 结构的父记录,并在创建变更集时将相应的记录 ID 传递给

  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

这个函数只是传递信息和returns变更集:

 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

在结构中,我定义了关系并将 user_idmerchant_id 正确地转换为 Transaction Ecto Struct 定义中的参数。

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

当我在我的测试用例中测试该功能时,它进入事务正常。

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

但是当我在 Phoenix 控制器中 运行 它时,它失败了。我不确定为什么会失败或要求预加载父关联。我应该在某个地方做一个 put_assoc 而不是直接将 user_idmerchant_id 作为参数吗?

问题是我作为 OTP 应用程序的设置无法共享 Ecto 的预加载方面。我在 Phoenix 的 web 端很好,但我无法在控制器中提交变更集,因为 Phoenix 应用程序中的 Repo 与 OTP 应用程序的变更集引用不同,这会导致预加载错误。

我提取它的原因是 ! 我在原始设置中错过了。

我将我的控制器改回看起来像这样 returns {:ok, transaction} 像这样

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

这是不同的,因为我没有 returning 变更集。我正在 OTP 应用程序中执行 Repo.insert 并 return 进行交易。

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

我改变这个的最初原因是因为我使用 |> MyRewards.Repo.insert! 而不是 |> MyRewards.Repo.insert...最后的 ! bang 改变了 return 函数只 return 结构而不是 return 状态和像 {:ok, transaction} 这样的结构,这是控制器所期望的。

简而言之,如果您将应用程序拆分为 OTP 和 Phoenix,请注意回购插入中的 !