我在一条类似的船上(使用 GraphQL),我选择尽可能远离cast_assoc。这不是因为“创建”场景,而是因为“更新”场景。
Looking at the documentation for cast_assoc,你会看到它说...
- 如果参数不包含ID,参数数据将通过新结构传递到changeset/2,成为插入操作
- 如果参数包含一个ID并且没有与该ID关联的子节点,则参数数据将通过新结构传递到changeset/2并成为插入操作
- 如果参数包含一个ID,并且有一个关联的孩子与这个ID,参数数据将通过现有结构传递到changeset/2,并成为一个更新操作
- 如果有关联的子 ID 且其 ID 未作为参数给出,则将调用该关联的 :on_replace 回调(请参阅模块文档中的“替换时”部分)
场景 1 是您的标准创建,这意味着您的数据需要与上面的嵌套输入看起来相似。 (它实际上需要是一个 list 的 maps 用于电子邮件键。
假设某人添加了第二封电子邮件(您在上面指出这是一对多)。如果您的输入如下所示:
%{
id: 12345,
username: "test",
emails: [
%{email: "test_2@123.com"}
}
}
...这会同时触发场景 1(新参数,无 ID)和场景 4(未给出 ID 的子节点)有效地删除所有先前的电子邮件。这意味着您的更新参数实际上需要如下所示:
%{
id: 12345,
username: "test",
emails: [
%{id: 1, email: "test@123.com"},
%{email: "test_2@123.com}
]
}
...对我来说,这意味着在请求中排队很多额外的数据。对于像电子邮件这样的东西——用户不太可能很少——成本很低。对于创建更丰富的关联,这是一种痛苦。
与其总是将cast_assoc 放入您的User.changeset,不如创建一个特定的变更集进行注册,它只使用一次强制转换:
defmodule MyApp.UserRegistration do
[...schema, regular changeset...]
def registration_changeset(params) do
%MyApp.User{}
|> MyApp.Repo.preload(:emails)
|> changeset(params)
|> cast_assoc(:emails, required: true, with: &MyApp.Email.changeset(&1, &2))
end
end
您仍然需要在您的输入中提供一个嵌套的 emails 字段,这可能很糟糕,但至少您不会使用 cast_assoc 污染您的普通用户变更集。
最后一个想法:与其让您的客户关心嵌套,您可以在特定于注册的解析器函数中这样做吗?