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_all
、Repo.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 的解决方案,我能够编译并工作。
值得注意的几点:
我必须在 up
和 down
函数中途调用 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
我有一个列 :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_all
、Repo.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 的解决方案,我能够编译并工作。
值得注意的几点:
我必须在
up
和down
函数中途调用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