Ecto - Правильный способ переноса поля в другой тип с использованием пользовательской логики? - PullRequest
0 голосов
/ 11 июля 2019

У меня есть столбец :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


Проблема в том, что мне также придется изменить field :from, {:array, :string} на field :from, :string в моей Mail схеме, и это вызывает проблемы с проверкой.

На шаге up Assistant.Repo.all() завершится ошибкой, поскольку Ecto не может загрузить поле from из старой БД из-за несоответствия типов.

На шаге down Assistant.Repo.update!(changeset) завершится ошибкой, поскольку Ecto.Changeset сообщил об ошибке несоответствия типов на :from.

В Rails на самом деле нет строгой проверки схемы, поэтому вы можете сойти с кода.

Как правильно выполнять такие миграции с помощью Ecto? Нет ли другого способа, кроме написания собственного SQL?

Ответы [ 2 ]

2 голосов
/ 11 июля 2019

Вы должны избегать использования Structs и Changeset в миграциях.Вместо этого используйте 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

Не уверен, что весь код чистый и компилируемый, но надеюсь, что моя идея ясна

0 голосов
/ 12 июля 2019

Основываясь на решении от apelsinka223, я смог заставить его скомпилироваться и работать.

Некоторые моменты, на которые стоит обратить внимание:

  • Мне пришлось позвонить flush() на полпути между функциями up и down, в противном случае удаление и добавление столбцов не произойдет вовремя.

  • Если запрос не основан насхема, для ее выполнения необходимо явно использовать оператор select в запросе к Ecto.

  • 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
...