You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Phoenix更新select2多选报Ecto.Changeset.cast/4无匹配子句错误

问题背景

更新select2多选组件绑定的关联数据时触发运行异常,相关代码、报错信息及修复方案如下:

Ecto Schema定义

defmodule Recon.Accounts.OriginatorMapping do
  use Ecto.Schema
  use Endon
  import Ecto.Changeset

  @timestamps_opts [autogenerate: {Recon.Reconmaster.Master.Localtime, :autogenerate, []}]
  schema "tbl_originator_mapping" do
    field :status, :string, default: "D"

    belongs_to :originator, Recon.Accounts.OriginatorIds, foreign_key: :org_id, type: :id
    belongs_to :participant, Recon.Accounts.Participant, foreign_key: :participants_id, type: :id
    belongs_to :maker, Recon.Accounts.OriginatorIds, foreign_key: :maker_id, type: :id
    belongs_to :checker, Recon.Accounts.Participant, foreign_key: :checker_id, type: :id

    timestamps()
  end

  @doc false
  def changeset(originator_mapping, attrs) do
    originator_mapping
    |> cast(attrs, [:org_id, :participants_id, :maker_id, :checker_id, :status])
    |> validate_required([])
  end
end

控制器更新逻辑代码

def update_originator_mapping(conn, params) do
    activity = "Modified Originator Mapping for "
    user = conn.assigns.user

    params["participants_id"]
    |> Enum.with_index()
    |> Enum.reduce(Ecto.Multi.new(), fn {_col, index}, multi ->
      column_params = %{
        maker_id: conn.assigns.user.id,
        checker_id: conn.assigns.user.id,
        participants_id: Enum.at(params["participants_id"], index),
        org_id: params["org_id"]
      }
      participant_id = Enum.at(params["participants_id"], index)
      column =
        if byte_size(participant_id) > 0, do: OriginatorMapping.find_by(org_id: params["org_id"], participants_id: participant_id), else: %OriginatorMapping{}
      changeset = OriginatorMapping.changeset(column, column_params)
      Ecto.Multi.insert_or_update(multi, {:column, index}, changeset)
    end)
    |> Ecto.Multi.insert(:user_log, %UserLog{user_id: user.id, activity: activity})
    |> Repo.transaction()
    |> case do
      {:ok, %{user_log: _user_log}} ->
        json(conn, %{data: "Operation Successfull!"})

      {:error, _failed_operation, failed_value, _changes_so_far} ->
        reason = traverse_errors(failed_value.errors) |> List.first()
        json(conn, %{error: reason})
    end
end

服务端报错日志

[error] #PID<0.1831.0> running ReconWeb.Endpoint (connection #PID<0.1671.0>, stream id 37) terminated
Server: localhost:4000 (http)
Request: POST /update/originator/mapping
** (exit) an exception was raised:
** (FunctionClauseError) no function clause matching in Ecto.Changeset.cast/4
(ecto 3.8.3) lib/ecto/changeset.ex:480: Ecto.Changeset.cast(nil, %{checker_id: 1, maker_id: 1, org_id: "1", participants_id: "3"}, [:org_id, :participants_id, :maker_id, :checker_id, :status], [])
(recon 0.1.0) lib/recon/accounts/originator_mapping.ex:24: Recon.Accounts.OriginatorMapping.changeset/2
(recon 0.1.0) lib/recon_web/controllers/originator_ids_controller.ex:353: anonymous fn/4 in ReconWeb.OriginatorIdsController.update_originator_mapping/2
(elixir 1.13.3) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
(recon 0.1.0) lib/recon_web/controllers/originator_ids_controller.ex:340: ReconWeb.OriginatorIdsController.update_originator_mapping/2
(recon 0.1.0) lib/recon_web/controllers/originator_ids_controller.ex:1: ReconWeb.OriginatorIdsController.action/2
(recon 0.1.0) lib/recon_web/controllers/originator_ids_controller.ex:1: ReconWeb.OriginatorIdsController.phoenix_controller_pipeline/2
(phoenix 1.5.13) lib/phoenix/router.ex:352: Phoenix.Router.call/2
(recon 0.1.0) lib/recon_web/endpoint.ex:1: ReconWeb.Endpoint.plug_builder_call/2
(recon 0.1.0) lib/plug/debugger.ex:136: ReconWeb.Endpoint."call (overridable 3)"/2
(recon 0.1.0) lib/recon_web/endpoint.ex:1: ReconWeb.Endpoint.call/2
(phoenix 1.5.13) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4
(cowboy 2.9.0) d:/Work/Recon/recon/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
(cowboy 2.9.0) d:/Work/Recon/recon/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
(cowboy 2.9.0) d:/Work/Recon/recon/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
(stdlib 3.14.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

根因分析

报错触发逻辑:传入Ecto.Changeset.cast/4的第一个参数为nil。Endon库的find_by方法在查询不到匹配记录时会返回nil,原代码仅对participant_id为空的场景做了空结构体兜底,没有处理查询无结果返回nil的情况,nil传入changeset后直接触发cast函数的子句匹配错误。

修复方法
  • 核心修复:对find_by的返回值做空值兜底,查询无结果时返回空%OriginatorMapping{}结构体,修改对应代码段:
column =
  if byte_size(participant_id) > 0 do
    OriginatorMapping.find_by(org_id: params["org_id"], participants_id: participant_id) || %OriginatorMapping{}
  else
    %OriginatorMapping{}
  end
  • 冗余逻辑优化:原代码使用Enum.with_index()遍历后又重复调用Enum.at按索引取当前遍历值属于冗余操作,直接解构遍历元组拿到当前participant_id即可:
|> Enum.reduce(Ecto.Multi.new(), fn {participant_id, index}, multi ->
  column_params = %{
    maker_id: conn.assigns.user.id,
    checker_id: conn.assigns.user.id,
    participants_id: participant_id,
    org_id: params["org_id"]
  }
  # 后续逻辑保持不变
end)

内容的提问来源于stack exchange,提问作者Miyoba Muchindu Sikabbubba

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 15:09:17