【问题标题】:How to update existing data in Ecto/Phoenix?如何更新 Ecto/Phoenix 中的现有数据?
【发布时间】:2020-05-11 20:27:00
【问题描述】:

我想根据现有数据更新条目中的字段。

我可以在变更集管道中定义方法吗?还是我需要先更新数据,然后再应用变更集?

例如,我有一个UUID 的用户。如果长度未达到 8,我需要用额外的“0”更新字段。

"123" => "00000123"

我知道我们总是可以生成新数据的映射,然后传递给变更集:

user = Repo.get(User, 1)
new_uuid = "00000#{user.uuid}" 

user
|> User.changeset(%{uuid: new_uuid})
|> Repo.update()

但我在想是否可以在变更集管道内执行逻辑?如果可以的话,代码怎么写?

user = Repo.get(User, 1)
user
|> <modify the data>
|> Repo.update()

【问题讨论】:

    标签: elixir phoenix-framework ecto


    【解决方案1】:

    @Alekseis 答案的问题在于语法:

    %{changeset | uuid: String.pad_leading(uuid, 8, "0")}
    

    首先,更改将存在于changeset.changes,例如changeset.changes.uuid。但它们仅在此属性实际发生更改时才存在。所以写

    %{changeset.changes | uuid: String.pad_leading(uuid, 8, "0")}
    

    对我们没有帮助,因为这种语法只有在键已经存在于地图中时才有效。我们可以这样做:

    Map.put(changeset, :uuid, String.pad_leading(uuid, 8, "0")
    

    但我建议使用来自Ecto.Changeset 的适当函数,因为变更集可能只是一个结构,但围绕它有相当多的逻辑。我们的朋友是:https://hexdocs.pm/ecto/Ecto.Changeset.html#put_change/3

    所以让我们重写解决方案:

    @spec prepend_zeroes_to_uuid(Ecto.Changeset.t()) :: Ecto.Changeset.t()
    # There are changes on uuid but it already is 8 characters long
    def prepend_zeroes_to_uuid(%{changes: %{uuid: uuid}} = changeset) 
      when is_binary(uuid) and byte_size(uuid) >= 8, do: changeset
    
    # There are changes on uuid and it needs padding
    def prepend_zeroes_to_uuid(%{changes: %{uuid: uuid}} = changeset) when is_binary(uuid) do
      put_change(changeset, :uuid, String.pad_leading(uuid, 8, "0"))
    end
    
    # There are no changes on uuid
    def prepend_zeroes_to_uuid(changeset), do: changeset
    
    # Now in your pipeline:
    user
    |> User.changeset(%{uuid: new_uuid})
    |> prepend_zeroes_to_uuid()
    |> Repo.update()
    

    【讨论】:

    • 谢谢奥莱。实际的问题是是否有可能在管道中获取现有数据。所以这里是关于如何构建new_uuid。我的答案是在管道之前构建new_uuid。但我在想我们是否可以在管道中构建它,因为我们已经将 user 作为参数传递了。
    • prepend_zeroes_to_uuid 的逻辑在问题中并不重要,它可以是任何东西,改变字符串、传递值或函数中的任何逻辑,所以没关系。
    【解决方案2】:

    没有魔法。 Ecto.Changeset 只是一个简单的结构体,所有的辅助函数只是简单地修改它,可能会添加错误和/或更改数据。

    也就是说,您可以使用 @spec 来实现函数

    @spec my_fun(Ecto.Changeset.t(), ...) :: Ecto.Changeset.t()
    

    并将其应用到管道中的任何位置(在设置UUID 值之后)。需要此签名才能使此函数可插入到管道中。

    有点像:

    @spec prepend_zeroes_to_uuid(Ecto.Changeset.t()) :: Ecto.Changeset.t()
    def prepend_zeroes_to_uuid(%{uuid: uuid} = changeset)
      when is_binary(uuid) and byte_size(uuid) >= 8, do: changeset
    
    def prepend_zeroes_to_uuid(%{uuid: uuid} = changeset)
        when is_binary(uuid) do
      %{changeset | uuid: String.pad_leading(uuid, 8, "0")}
    end
    
    def prepend_zeroes_to_uuid(changeset), do: add_error(...)
    

    并将其包含到管道中。

    user = Repo.get(User, 1)
    user
    |> changeset ...
    ...
    |> prepend_zeroes_to_uuid()
    |> Repo.update()
    

    要直接更新User 架构,可以对其应用相同的函数。

    @spec prepend_zeroes_to_uuid(User.t()) :: User.t()
    

    其余的保持不变。

    【讨论】:

    • 谢谢,你能告诉我如何调用这些方法吗?假设我们已经得到了user
    • 在设置UUID 之后将其放在管道中的任何位置。 例如直接在cast/3 之后。或在validate/2 之后。或任何适用的地方。我已经更新了答案。
    • 嗨,我想有人在... 地方给我解决方案谢谢。
    • 我不关注。你不清楚我的回答到底是什么?
    • 我遇到的问题是changeset 没有包含数据。只有数据改变了才能有数据。
    【解决方案3】:

    在做了一些研究之后,我找到了一些解决我的问题的方法,但我会留下这个问题,看看是否有更好的答案。

    Changeset 专为新数据或更改数据而设计。如果我们只是从 params 获取数据,或者我们已经有了新数据,我们可以使用 changeset 在推送到数据库之前验证和更新数据。

    但是,它不能用于验证/更新现有数据(即有效规则已更改,我们需要更新现有数据)。那是我的问题。

    因此,我没有使用变更集来获取有效数据,而是从数据库中获取数据,更改数据,然后推送到变更集并更新数据库。

    对于我的示例,我们获取 uuid 并将其更新为 new_uuid,并将其也传递给变更集。

    user = Repo.get(User, 1)
    new_uuid = "00000#{user.uuid}" 
    
    user
    |> User.changeset(%{uuid: new_uuid})
    |> Repo.update()
    

    我不确定这个解决方案是否是最佳实践,如果有人有更好的解决方案来更新现有数据,请发布您的答案,谢谢。

    【讨论】:

      猜你喜欢
      • 2016-11-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-01
      相关资源
      最近更新 更多