我应该如何管理插入到相关表中

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")

我需要:

  1. 重新格式化我的输入,以便将其传递到我的 ecto 变更集中(将其嵌套在电子邮件中)
  2. 展平我的变更集错误以便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