如何在 Ecto 迁移中动态更新字段值?

How to update field value dynamically in Ecto migration?

我有一个用户table喜欢:

     email     | username
---------------+----------
 123@321.com   |
 123@123.com   |
 haha@haha.com |

我想用 email 字段更新 username 字段,只需在 @.

之前切片 email
     email     | username
---------------+----------
 123@321.com   | 123
 123@123.com   | 123
 haha@haha.com | haha

我尝试使用以下迁移:

defmodule MyApp.Repo.Migrations.AddDefaultUsernameForUsers do
  use Ecto.Migration
  import Ecto.Query

  def up do
      from(u in MyApp.User, update: [set: [username: String.split(u.email, "@") |> List.first ]])
        |> MyApp.Repo.update_all([])
  end

  def down do
      MyApp.Repo.update_all(MyApp.User, set: [username: nil])
  end
end

但是在运行迁移时,出现以下错误:

$ mix ecto.migrate
** (Ecto.Query.CompileError) `List.first(String.split(u.email(), "@"))` is not a valid query expression

我该如何解决这个问题?

您将要进行两个单独的查询。一个查询获取数据,做任何你想做的改变,然后第二个查询更新数据。类似于

Repo.all(MyApp.User)
|> Enum.map(fn u ->
  username = 
    u.email
    |> String.split("@")
    |> List.first()

  Ecto.Changeset.cast(u, %{username: username})
end)
|> Repo.update_all()

关于为什么你不能做你想做的事情,有几件事正在发生。

当您想在 Ecto 查询中使用 Elixir 函数或值时,通常必须使用 pin 运算符 (^)。所以如果你想查询一个特定的 ID,你可以使用 from(u in MyApp.User, where: u.id == ^12)。所以你的反应可能是尝试使用 ^List.first(String.split(u.email, "@"))。但是,这不会起作用,因为...

from(u in MyApp.User)中的u是数据库中的记录。您无权在您的 Elixir 代码中访问它。可以使用 fragment/1 来做你想做的事情,但是你不能用常规的 Elixir 函数来操作这个值,直到你使用像我上面的例子那样把它从数据库中拉出来。

@Justin Wood 已经解释了为什么不能在更新查询中使用 Elixir 函数,所以我不再重复。在 PostgreSQL 中,您可以使用带有正则表达式的 substring 函数提取 @ 之前的文本,这将与更新查询一起使用。这比加载记录然后一条一条地更新它们要快得多,但是如果不调整 SQL 片段就不能与其他数据库引擎一起使用:

from(u in MyApp.User,
  update: [set: [username: fragment("substring(? from '^(.*?)@')", u.email)]])
|> MyApp.Repo.update_all([])
postgres=# select substring('123@321.com' from '^(.*?)@');
 substring
-----------
 123
(1 row)