【问题标题】:How should I manage inserting into related tables我应该如何管理插入相关表
【发布时间】:2018-11-09 21:36:08
【问题描述】:

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

目前,我的帐户变更集使用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。

【问题讨论】:

    标签: elixir graphql phoenix-framework ecto absinthe


    【解决方案1】:

    您的问题涉及应用程序中的不同点,因此我假设您使用的是 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
    

    【讨论】:

    • 感谢您的回复!这与我现在正在做的很接近,但我也想找回验证错误。 traverse_errors/2 仍然返回 一系列嵌套映射 中的错误,所以我最终得到 emails: {email: "has invalid format"}。我试图以 {field, message} 对的形式返回错误,所以我最终会返回一个“emails.email”字段(通常是扁平化变更集),除非我还修改了 register 解析器中的变更集(我'我遇到了麻烦,因为我是 elixir 的新手)。所以按摩输入效果很好,但按摩输出就不那么好了。
    • 您可以采取的另一种方法是创建一个embedded_schema 用于注册,它是自己的变更集,这样当返回给客户端时变更集错误看起来“平坦”。一旦一切都是valid?: true,您就可以将数据拆分为不同的、由数据库支持的模式
    • @cnorris 看来问题可以通过调整error_details/1中的逻辑来解决。你同意吗?
    【解决方案2】:

    我在一条类似的船上(使用 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 是您的标准创建,这意味着您的数据需要与上面的嵌套输入看起来相似。 (它实际上需要是一个 listmaps 用于电子邮件键。

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

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

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

    【讨论】:

    • 谢谢 这听起来像是一场噩梦。我已根据您的建议分离了我的注册变更集,但为了处理您上面概述的更新场景,您是否只是删除 cast_assoc 并使用事务(多),否则您将如何更新相关数据?
    • 在我的应用程序中,我正在做 phone_numbers 类似于你在做电子邮件的方式(只是因为那是我的业务逻辑)。它总是使用user_id 创建或删除电话号码,而不是更新电话号码。这破坏了“一种形式做所有事情”的现象,但我说的很好。您可以采取的另一种方法是将所有内容包装在事务中的单独模块
    • 我会说,如果你的关系是has_one,我认为cast_assoc 更有意义,因为在更新中,根据定义,你想要旧的一个离开。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-18
    • 2021-03-25
    • 2016-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多