我应该如何管理插入到相关表中
How should I manage inserting into related tables
我有一个帐户table,链接到一个电子邮件table,大致如下:
目前,我的帐户变更集使用 cast_assoc
提取电子邮件:
|> cast_assoc(:emails, required: true, with: &Email.changeset(&1, &2))
但这意味着我需要提供如下数据:
%{
username: "test",
password: "secret",
emails: %{email: "test@123.com"} //<- nested
}
我正在使用 GraphQL,为了支持 "register" 形式的变化:
register(username:"test", password:"secret", email: "test@test.com")
我需要:
- 重新格式化我的输入,以便将其传递到我的 ecto 变更集中(将其嵌套在电子邮件中)
- 展平我的变更集错误以便return验证消息
有没有办法重构它,或者修改我的变更集以取消嵌套该字段?我是 elixir 和 ecto 的新手。
我在一条类似的船上(使用 GraphQL),我选择尽可能远离 cast_assoc
。这较少是因为“创建”场景,更多是因为“更新”场景。
Looking at the documentation for cast_assoc
,你会看到它说...
- If the parameter does not contain an ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
- If the parameter contains an ID and there is no associated child with such ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
- If the parameter contains an ID and there is an associated child with such ID, the parameter data will be passed to changeset/2 with the existing struct and become an update operation
- If there is an associated child with an ID and its ID is not given as parameter, the :on_replace callback for that association will be invoked (see the “On replace” section on the module documentation)
场景 1 是您的标准创建,这意味着您的数据需要看起来 类似于 上面的嵌套输入。 (它实际上需要是 list of maps 作为电子邮件密钥。
假设某人添加了第二封电子邮件(您在上面指出这是 one-to-many)。如果您的输入如下:
%{
id: 12345,
username: "test",
emails: [
%{email: "test_2@123.com"}
}
}
...这会触发场景 1(新参数,无 ID) 和 场景 4(未提供 ID 的 child)有效地删除所有先前的电子邮件。这意味着您的更新参数实际上需要看起来像:
%{
id: 12345,
username: "test",
emails: [
%{id: 1, email: "test@123.com"},
%{email: "test_2@123.com}
]
}
...对我来说,这意味着在请求中排队大量额外数据。对于像电子邮件这样的东西——用户不太可能拥有很少的东西——成本很低。对于more-abundantly-created协会,痛苦。
与其总是将 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 污染您的普通用户变更集。
最后一个想法:与其让您的客户关心嵌套,不如在您的 registration-specific 解析器函数中做到这一点?
您的问题涉及应用程序中的不同点,因此我假设您使用的是 Phoenix >= 1.3 以及 Absinthe。这样,我们就可以讨论您的上下文和您的解析器可能是什么样子。
处理传入的 GraphQL 请求涉及在到达域模块中的变更集函数之前经过两个抽象级别:首先,解析器;然后是上下文模块。一个重要的好的做法是 你的解析器应该只调用上下文函数 。这个想法是让解析器与 Ecto 模式所在的底层域模块分离。
然后您可以使用解析器处理您的输入,使其适合您的上下文函数所期望的任何内容。假设您的上下文被命名为 Accounts
,您的解析器可能看起来有点像这样:
def register(_root, %{username: username, password: password, email: email}, _info) do
args = %{username: username, password: password, emails: [%{email: email}]}
case Accounts.create_account(args) do
{:ok, %Account{} = account} ->
{:ok, account}
{:error, changeset} ->
{:error, message: "Could not register account", details: error_details(changeset)}
end
end
然后调用这个依赖于 traverse_errors/2
的简单辅助函数到 return 所有验证消息:
defp error_details(changeset) do
changeset
|> Ecto.Changeset.traverse_errors(fn {msg, _} -> msg end)
end
我有一个帐户table,链接到一个电子邮件table,大致如下:
目前,我的帐户变更集使用 cast_assoc
提取电子邮件:
|> cast_assoc(:emails, required: true, with: &Email.changeset(&1, &2))
但这意味着我需要提供如下数据:
%{
username: "test",
password: "secret",
emails: %{email: "test@123.com"} //<- nested
}
我正在使用 GraphQL,为了支持 "register" 形式的变化:
register(username:"test", password:"secret", email: "test@test.com")
我需要:
- 重新格式化我的输入,以便将其传递到我的 ecto 变更集中(将其嵌套在电子邮件中)
- 展平我的变更集错误以便return验证消息
有没有办法重构它,或者修改我的变更集以取消嵌套该字段?我是 elixir 和 ecto 的新手。
我在一条类似的船上(使用 GraphQL),我选择尽可能远离 cast_assoc
。这较少是因为“创建”场景,更多是因为“更新”场景。
Looking at the documentation for cast_assoc
,你会看到它说...
- If the parameter does not contain an ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
- If the parameter contains an ID and there is no associated child with such ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
- If the parameter contains an ID and there is an associated child with such ID, the parameter data will be passed to changeset/2 with the existing struct and become an update operation
- If there is an associated child with an ID and its ID is not given as parameter, the :on_replace callback for that association will be invoked (see the “On replace” section on the module documentation)
场景 1 是您的标准创建,这意味着您的数据需要看起来 类似于 上面的嵌套输入。 (它实际上需要是 list of maps 作为电子邮件密钥。
假设某人添加了第二封电子邮件(您在上面指出这是 one-to-many)。如果您的输入如下:
%{
id: 12345,
username: "test",
emails: [
%{email: "test_2@123.com"}
}
}
...这会触发场景 1(新参数,无 ID) 和 场景 4(未提供 ID 的 child)有效地删除所有先前的电子邮件。这意味着您的更新参数实际上需要看起来像:
%{
id: 12345,
username: "test",
emails: [
%{id: 1, email: "test@123.com"},
%{email: "test_2@123.com}
]
}
...对我来说,这意味着在请求中排队大量额外数据。对于像电子邮件这样的东西——用户不太可能拥有很少的东西——成本很低。对于more-abundantly-created协会,痛苦。
与其总是将 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 污染您的普通用户变更集。
最后一个想法:与其让您的客户关心嵌套,不如在您的 registration-specific 解析器函数中做到这一点?
您的问题涉及应用程序中的不同点,因此我假设您使用的是 Phoenix >= 1.3 以及 Absinthe。这样,我们就可以讨论您的上下文和您的解析器可能是什么样子。
处理传入的 GraphQL 请求涉及在到达域模块中的变更集函数之前经过两个抽象级别:首先,解析器;然后是上下文模块。一个重要的好的做法是 你的解析器应该只调用上下文函数 。这个想法是让解析器与 Ecto 模式所在的底层域模块分离。
然后您可以使用解析器处理您的输入,使其适合您的上下文函数所期望的任何内容。假设您的上下文被命名为 Accounts
,您的解析器可能看起来有点像这样:
def register(_root, %{username: username, password: password, email: email}, _info) do
args = %{username: username, password: password, emails: [%{email: email}]}
case Accounts.create_account(args) do
{:ok, %Account{} = account} ->
{:ok, account}
{:error, changeset} ->
{:error, message: "Could not register account", details: error_details(changeset)}
end
end
然后调用这个依赖于 traverse_errors/2
的简单辅助函数到 return 所有验证消息:
defp error_details(changeset) do
changeset
|> Ecto.Changeset.traverse_errors(fn {msg, _} -> msg end)
end