Elixir Ecto反范式库调用cast_assoc插入关联数据报a_id非空错误
Ecto嵌套cast_assoc反范式冗余字段填充方案
问题场景
高性能模块采用反范式设计数据库,共定义3个Ecto Schema:
defmodule A do schema "a" do has_many :b, B end def changeset(a, attrs) do ... cast_assoc(a, :b) end end defmodule B do # db columns: b_id schema "b" do belongs_to :a, A has_many :c, C end def changeset(b, attrs) do ... cast_assoc(b, :c) end end defmodule C do # db_columns: b_id, a_id schema "c" do belongs_to :b, B belongs_to :a, A # 反范式冗余字段 end def changeset(c, attrs) do ... end end
预期通过A模块的changeset调用cast_assoc一次性插入B、C两层关联数据,执行时抛出Postgrex非空约束错误:
** (Postgrex.Error) ERROR 23502 (not_null_violation) null value in column "a_id" of relation "c" violates not-null constraint
问题成因:cast_assoc默认只会给子关联传递直接父级的外键,不会自动填充C表中反范式冗余存储的a_id字段,最终触发非空约束。
无需Ecto.Multi的实现方案
不需要引入Ecto.Multi,直接在中间层B的changeset中做参数透传即可,核心逻辑是利用cast_assoc执行时父级外键已经完成填充的特性,提前把冗余外键注入到子关联参数中。
- 修改B模块的changeset,在处理C关联参数前注入
a_id
defmodule B do schema "b" do belongs_to :a, A has_many :c, C end def changeset(b, attrs) do # 从当前B的变更/存量数据中取出a_id a_id = get_field(b, :a_id) # 预处理C关联参数,补全a_id attrs = inject_a_id_to_c(attrs, a_id) b |> cast(attrs, []) # 此处填写B自身需要cast的字段列表 |> cast_assoc(:c) end # a_id为空时直接返回原参数 defp inject_a_id_to_c(attrs, nil), do: attrs # 兼容map格式参数 defp inject_a_id_to_c(attrs, a_id) when is_map(attrs) do c_params = Map.get(attrs, "c") || Map.get(attrs, :c) if is_nil(c_params) do attrs else processed_c = Enum.map(c_params, fn param -> param |> Map.put_new(:a_id, a_id) |> Map.put_new("a_id", a_id) end) attrs |> Map.delete("c") |> Map.put(:c, processed_c) end end # 兼容关键字列表格式参数 defp inject_a_id_to_c(attrs, a_id) when is_list(attrs) do c_params = Keyword.get(attrs, :c) if is_nil(c_params) do attrs else processed_c = Enum.map(c_params, fn param -> Keyword.put_new(param, :a_id, a_id) end) Keyword.put(attrs, :c, processed_c) end end end
- 补全C模块changeset的字段处理逻辑,确保
a_id能正确写入
defmodule C do schema "c" do belongs_to :b, B belongs_to :a, A end def changeset(c, attrs) do c |> cast(attrs, [:b_id, :a_id]) |> validate_required([:b_id, :a_id]) end end
注意事项:
- 用
put_new而非put注入字段,避免手动传入的a_id被覆盖 - 仅在新增关联时注入值即可,更新场景下存量C数据已经有
a_id,不会触发非空错误 - 如果有更多层反范式字段需要透传,按照同样的逻辑在每一层父changeset预处理子参数即可,不需要修改Ecto原生行为
内容的提问来源于stack exchange,提问作者Hoggie
相关产品推荐
相关产品推荐

