Ecto - 使用自定义逻辑将字段迁移到不同类型的正确方法?

Ecto - The right way of migrating a field to a different type using custom logic?

我有一个列 :from,它最初是 {:array, :string} 类型。现在我想将此列迁移为 :string 类型,将数组的第一个条目作为新值。

在Rails中,您可以在迁移中使用一些自定义逻辑来完成。我正在尝试对 Ecto 做同样的事情,但是由于模式验证和变更集错误 运行 遇到了问题。

defmodule Assistant.Repo.Migrations.ChangeFromFieldOnMails do
  use Ecto.Migration

  def up do
    dict_of_froms =
      Assistant.Mail
      |> Assistant.Repo.all()
      |> Enum.reduce(%{}, fn mail, acc ->
        Map.put(acc, mail.id, List.first(mail.from))
      end)

    alter table(:mails) do
      remove :from
      add :from, :string
    end

    Assistant.Mail
    |> Assistant.Repo.all()
    |> Enum.each(fn mail ->
      changeset = Ecto.Changeset.change(mail, from: Map.get(dict_of_froms, mail.id))
      Assistant.Repo.update!(changeset)
    end)
  end

  def down do
    dict_of_froms =
      Assistant.Mail
      |> Assistant.Repo.all()
      |> Enum.reduce(%{}, fn mail, acc ->
        Map.put(acc, mail.id, [mail.from])
      end)

    alter table(:mails) do
      remove :from
      add :from, {:array, :string}
    end

    Assistant.Mail
    |> Assistant.Repo.all()
    |> Enum.each(fn mail ->
      changeset = Ecto.Changeset.change(mail, from: Map.get(dict_of_froms, mail.id))
      Assistant.Repo.update!(changeset)
    end)
  end
end


问题是,我还必须在我的 Mail 架构中将 field :from, {:array, :string} 更改为 field :from, :string,这会导致验证出现问题。

up 步骤中,Assistant.Repo.all() 会失败,因为 Ecto 由于类型不匹配而无法从旧数据库加载 from 字段。

down 步骤中,Assistant.Repo.update!(changeset) 会失败,因为 Ecto.Changeset:from 上报告了类型不匹配错误。

在 Rails 中,并没有真正严格地检查架构,因此您可以不用代码。

使用 Ecto 执行此类迁移的正确方法是什么?除了写自定义 SQL?

就没有别的办法了吗

您需要避免在迁移中使用结构和变更集。请改用 Repo.insert_allRepo.update_all 和模式命名。

defmodule Assistant.Repo.Migrations.ChangeFromFieldOnMails do
  use Ecto.Migration
  import Ecto.Query

  def up do
    dict_of_froms =
      "mails"                # table name as string
      |> Assistant.Repo.all()
      |> Enum.reduce(%{}, fn mail, acc ->
        Map.put(acc, mail.id, List.first(mail.from))
      end)

    alter table(:mails) do
      remove :from
      add :from, :string
    end

    dict_of_froms
    |> Enum.each(fn {id, from} ->   # changed this cycle little bit, so it would
         "mails"                    # update record only if we have `from` for it
         |> where(id: ^id)
         |> update(set: [from: ^from])
         |> Repo.update_all()
    end) 
  end

  def down do
    dict_of_froms =
      "mails"
      |> Assistant.Repo.all()
      |> Enum.reduce(%{}, fn mail, acc ->
        Map.put(acc, mail.id, [mail.from])
      end)

    alter table(:mails) do
      remove :from
      add :from, {:array, :string}
    end

    dict_of_froms
    |> Enum.each(fn {id, from} ->   # changed this cycle little bit, so it would
         "mails"                    # update record only if we have `from` for it
         |> where(id: ^id)
         |> update(set: [from: ^from])
         |> Repo.update_all()
    end) 
  end
end

不确定所有代码是否干净且可编译,但希望我的想法很清楚

基于 apelsinka223 的解决方案,我能够编译并工作。

值得注意的几点:

  • 我必须在 updown 函数中途调用 flush(),否则无法及时删除和添加列。

  • 如果查询不是基于模式,则需要在 Ecto 的查询中显式使用 select 语句来 运行 它。

  • update_all() 至少需要两个参数。可以传入 [] 作为第二个参数。

defmodule Assistant.Repo.Migrations.ChangeFromFieldOnMails do
  use Ecto.Migration
  import Ecto.Query, only: [from: 2]
  alias Assistant.Repo

  def up do
    query = from(m in "mails", select: {m.id, m.from})

    dict_of_froms =
      query
      |> Repo.all()
      |> Enum.reduce(%{}, fn {id, from}, acc ->
        Map.put(acc, id, List.first(from))
      end)

    alter table(:mails) do
      remove :from
      add :from, :string
    end

    flush()

    dict_of_froms
    |> Enum.each(fn {id, fr} ->
      query =
        from(m in "mails",
          where: m.id == ^id,
          update: [set: [from: ^fr]]
        )

      Repo.update_all(query, [])
    end)
  end

  def down do
    query = from(m in "mails", select: {m.id, m.from})

    dict_of_froms =
      query
      |> Repo.all()
      |> Enum.reduce(%{}, fn {id, from}, acc ->
        Map.put(acc, id, [from])
      end)

    alter table(:mails) do
      remove :from
      add :from, {:array, :string}
    end

    flush()

    dict_of_froms
    |> Enum.each(fn {id, fr} ->
      query =
        from(m in "mails",
          where: m.id == ^id,
          update: [set: [from: ^fr]]
        )

      Repo.update_all(query, [])
    end)
  end
end