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

Elixir Ecto中embeds_one如何关联多个不同的embedded schema

错误原因

Ecto 原生的embeds_one并不支持either参数,你看到的相关用法属于非官方的野路子实现,官方文档从未收录该参数。你将either: [ Product, Service]传给了embeds_one本该填写embedded schema模块的位置,Ecto运行时会把这个keyword列表当做模块调用内置方法,自然就触发了你遇到的参数错误。

正确实现方式

不需要依赖不存在的either参数,手动处理动态类型的embedded schema逻辑即可,完整实现如下:

1. 调整Schema定义

schema "items" do
  field :name, :string
  # 用枚举约束type的取值范围,比普通字符串更安全
  field :type, Ecto.Enum, values: [:product, :service, :task]
  # 直接定义details为map类型,不绑定固定embedded schema
  field :details, :map

  # 可选:加after_load钩子,读取数据时自动把details转为对应struct
  after_load :load_details_struct
end

2. 实现动态cast逻辑

首先确保你的Product/Service/Task三个embedded schema已经定义好各自的changeset验证规则,然后在Item模块补充如下方法:

@doc false
def changeset(item, attrs) do
  item
  |> cast(attrs, [:name, :type])
  |> validate_required([:name, :type])
  |> then(fn changeset ->
    # 拿cast后的type值,避免直接取attrs时字符串key/原子key不匹配的问题
    current_type = get_change(changeset, :type) || changeset.data.type
    details_attrs = attrs[:details] || attrs["details"]
    cast_dynamic_details(changeset, current_type, details_attrs)
  end)
end

# 动态cast不同类型的details
defp cast_dynamic_details(changeset, _type, nil), do: changeset
defp cast_dynamic_details(changeset, type, details_attrs) do
  # 根据type匹配对应的embedded schema
  details_schema = case type do
    :product -> Product
    :service -> Service
    :task -> Task
  end

  # 处理新建/更新场景的初始值
  existing_details = changeset.data.details || struct(details_schema)
  details_changeset = details_schema.changeset(existing_details, details_attrs)

  if details_changeset.valid? do
    put_change(changeset, :details, apply_changes(details_changeset))
  else
    # 把embedded schema的错误合并到父changeset
    Enum.reduce(details_changeset.errors, changeset, fn {field, {msg, opts}}, acc ->
      add_error(acc, :details, "#{field} #{msg}", opts)
    end)
  end
end

# after_load钩子实现,读取时自动转struct
defp load_details_struct(item) do
  details_schema = case item.type do
    :product -> Product
    :service -> Service
    :task -> Task
  end
  %{item | details: struct(details_schema, item.details)}
end

可选优化

如果需要保持embeds_one的相关API能力,可以自定义一个Ecto类型来封装动态类型匹配逻辑,不需要修改上层调用规则。

内容的提问来源于stack exchange,提问作者sooon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 11:39:04