嵌套资源中祖父模型传值:基于Event计算Participant费用
如何在嵌套资源保存时根据关联Event计算Participant的费用?
你的当前方案不可行,核心问题是:在嵌套表单提交的场景下,Participant的changeset无法直接获取到Event上下文——你的populate_fees函数尝试从params的"event"键提取Event,但实际提交的参数结构里,Event的信息只会通过Registration的event_id传递,Participant的参数中不会包含event字段,导致那个分支永远不会被触发,fees字段无法被正确赋值。
下面是两种推荐的实现方式:
方案1:在Registration的changeset中传递Event给Participant的changeset
这种方式是在处理Registration的嵌套关联时,先获取对应的Event,再把它注入到Participant的参数中,让Participant的changeset可以拿到Event信息来计算费用:
修改Registration的changeset
def changeset(%Registration{} = registration, attrs) do # 根据event_id获取对应的Event(如果是新建Registration,从attrs取event_id;如果是更新,从已有记录取) event = if registration.event_id do Eventyr.Events.get_event!(registration.event_id) else Eventyr.Events.get_event!(attrs["event_id"]) end registration |> cast(attrs, [:conf_number, :event_id]) |> validate_required([:conf_number, :event_id]) # 在cast_assoc时,手动调用Participant的changeset并注入event参数 |> cast_assoc(:participants, required: true, fn participant, participant_attrs -> Participant.changeset(participant, Map.put(participant_attrs, "event", event)) end) |> unique_constraint(:conf_number) end
修改Participant的populate_fees函数
调整分支匹配,确保能识别传入的Event结构体,并处理age为空的边界情况:
defp populate_fees(%Ecto.Changeset{data: %{id: id}} = changeset) when is_integer(id) do changeset end defp populate_fees(%{params: %{"event" => %Eventyr.Events.Event{} = event}} = changeset) do # 优先取修改后的age,没有则取原有值 age = get_change(changeset, :age) || get_field(changeset, :age) fees = cond do age in 0..5 -> event.fees_first age in 6..16 -> event.fees_second age > 16 -> event.fees_third true -> Decimal.new(0) # 处理age为空或无效的情况,避免报错 end put_change(changeset, :fees, fees) end defp populate_fees(changeset) do changeset end
方案2:使用prepare_changes在Registration的changeset中批量设置Participant费用(更安全)
这种方式不需要修改Participant的changeset逻辑,而是在Registration的changeset最后阶段,通过prepare_changes遍历所有Participant的changeset,直接根据数据库中真实的Event信息设置费用,避免用户篡改参数的风险:
def changeset(%Registration{} = registration, attrs) do registration |> cast(attrs, [:conf_number, :event_id]) |> validate_required([:conf_number, :event_id]) |> cast_assoc(:participants, required: true) |> prepare_changes(fn changeset -> # 从changeset中获取event_id,查询数据库中的真实Event记录 event_id = get_field(changeset, :event_id) || get_change(changeset, :event_id) event = Eventyr.Events.get_event!(event_id) # 遍历所有Participant的changeset,设置对应的fees Enum.each(get_field(changeset, :participants), fn participant_changeset -> age = get_change(participant_changeset, :age) || get_field(participant_changeset, :age) fees = cond do age in 0..5 -> event.fees_first age in 6..16 -> event.fees_second age > 16 -> event.fees_third true -> Decimal.new(0) end put_change(participant_changeset, :fees, fees) end) changeset end) |> unique_constraint(:conf_number) end
这种方式的优势在于:Event信息是直接从数据库查询的,不会被用户提交的参数篡改,安全性更高;同时不需要修改Participant的changeset逻辑,代码耦合度更低。
额外注意事项
- 确保
Eventyr.Events.get_event!/1函数能正确通过event_id获取Event记录,如果需要处理不存在的情况,可以换成get_event/1并添加错误处理逻辑。 - 根据业务需求,决定在更新Participant时是否需要重新计算费用——如果需要,可以去掉Participant changeset中
populate_fees的第一个分支(仅当已有id时跳过的逻辑)。
内容的提问来源于stack exchange,提问作者Gorav Bhootra
相关产品推荐
相关产品推荐

