4

我有一个链接到电子邮件表的帐户表,大致如下:

在此处输入图像描述

目前,我的帐户变更集cast_assoc用于提取电子邮件:

|> cast_assoc(:emails, required: true, with: &Email.changeset(&1, &2))

但这意味着我需要提供如下数据:

%{
    username: "test",
    password: "secret",
    emails: %{email: "test@123.com"} //<- nested
}

我正在使用 GraphQL,并且为了支持以下形式的“注册”突变:

register(username:"test", password:"secret", email: "test@test.com")

我需要:

  1. 重新格式化我的输入,以便将其传递到我的 ecto 变更集中(将其嵌套在电子邮件中)
  2. 展平我的变更集错误以返回验证消息

有没有办法重构这个,或者修改我的变更集以取消嵌套该字段?我是长生不老药和ecto的新手。

4

2 回答 2

4

您的问题涉及应用程序中的不同点,因此我假设您使用的是 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返回所有验证消息:

defp error_details(changeset) do
  changeset
  |> Ecto.Changeset.traverse_errors(fn {msg, _} -> msg end)
end
于 2018-05-31T06:53:25.603 回答
1

我在一条类似的船上(使用 GraphQL),我选择尽可能远离cast_assoc。这不是因为“创建”场景,而是因为“更新”场景。

查看 的文档cast_assoc,您会看到它说...

  • 如果参数不包含ID,则参数数据将通过新结构传递到changeset/2,成为插入操作
  • 如果参数包含一个 ID 并且没有与该 ID 关联的子节点,则参数数据将通过新结构传递到 changeset/2 并成为插入操作
  • 如果参数包含一个 ID 并且有一个关联的子 ID 与该 ID,参数数据将通过现有结构传递到 changeset/2 并成为更新操作
  • 如果有一个带有 ID 的关联子节点并且其 ID 未作为参数给出,则将调用该关联的 :on_replace 回调(请参阅模块文档中的“替换时”部分)

场景 1 是您的标准创建,这意味着您的数据需要看起来类似于上面的嵌套输入。(它实际上需要是电子邮件密钥的映射列表。

假设一个人添加了第二封电子邮件(您在上面指出这是一对多)。如果您的输入如下所示:

%{
  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 污染你的普通用户变更集。

最后一个想法:与其让您的客户关心嵌套,您可以在特定于注册的解析器函数中这样做吗?

于 2018-05-31T05:27:59.733 回答