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

