Data migrations para Phoenix
Tags: elixir, ecto, backend, phoenix
Published: 2022-08-29 18:55:00

## O que são?

Migrations estão geralmente relacionadas a alterações na estrutura do banco de dados, mas é recorrente em ambientes de produção termos também a necessidade de efetuar operações em nossos dados. Chamaremos isso de data migrations. Nessas situações, é comum fazer essas data migrations através do mecanismo das migrations normais, as estruturais, ou até mesmo se conectar diretamente ao banco de dados e executar um SQL com as operações desejadas.

Mas…

Executar a SQL direto no banco não é a melhor das ideias, pois incentiva a falta de code review nessas operações, e é bastante propenso a erro humano, como esquecer uma transação aberta ou fazer um update sem where. Já fazer essas migrações junto das tradicionais migrações de estrutura, é um [code smell conhecido](https://github.com/lucasvegi/Elixir-Code-Smells#data-manipulation-by-migration).

## A solução

Uma solução recomendada é criar Mix Scripts para essas operações em dados, mas com [Mix Releases](https://hexdocs.pm/mix/1.13/Mix.Tasks.Release.html) não temos o Mix disponível, pois o Mix é uma ferramenta de desenvolvimento e build, e no binário gerado de uma release o objetivo é não incluir nada que não seja estritamente necessário para que o projeto seja executado. Aqui na Trybe, em vários serviços não temos nem Elixir nem Erlang instalados dentro dos containers — tudo é executado a partir do binário gerado pela release. 

Para contornar isso, dentro do nosso serviço responsável por projetos, adotamos uma solução levemente customizada para nossas data migrations.


Nosso serviço de projetos é uma aplicação Phoenix, e nela já tínhamos um módulo chamado Release, responsável por executar as migrações tradicionais, e como a própria [documentação](https://hexdocs.pm/phoenix/releases.html) diz, esse é o local perfeito para adicionar qualquer tipo de comando customizado que venha precisar ser executado em produção!

No módulo `Release`, e seguindo o esqueleto da função `migrate/0` que já temos por padrão, podemos ter uma função que ficará responsável por executar data migrations no nosso serviço.

### Primeiro, vamos definir como serão nossas data migrations!

* Elas devem estar em um diretório separado.
  Podemos colocar essas data migrations em um diretório chamado `data_migrations`, ao lado do nosso diretório `migrations` tradicional.
* Elas vão receber um Repo para fazer suas operações no banco de dados.
  Significa que não precisaremos acessar diretamente o módulo Repo da nossa aplicação, receberemos ele como parâmetro de uma função. Já podemos definir que essa função deverá ser chamada `run/1`.
* Elas podem ser executadas “n” vezes, individualmente, e não possuem uma ordem específica.

### Com essas premissas já podemos começar nossa implementação:

Essa é a função `migrate/0` do nosso módulo Release:
```elixir
def migrate do
  load_app()

  for repo <- repos() do
    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
  end
end
```

A função `migrate/0` é responsável por:
1. Garantir o carregamento da aplicação;
2. Pegar uma lista de todos os repos disponível através da `repos/0`;
3. Pedir para que o ecto execute as migrações pendentes para cada repo disponível;

Bem simples, nossa função customizada para migrações de dados reaproveitará os passos 1 e 2, só se diferenciando no 3, que irá executar uma data migration a partir de seu nome de arquivo.

Nossa implementação fica assim:

```elixir
  def migrate_data(file_name) do
    load_app()

    for repo <- repos() do
      with {:ok, migration} <- eval_data_migration(repo, file_name),
           {:ok, _, _} <- Ecto.Migrator.with_repo(repo, &migration.run(&1)) do
        Logger.info("A migração de dados foi executada.")
      else
        {:error, message} -> Logger.error(inspect(message))
      end
    end
  end

  defp eval_data_migration(repo, file_name) do
    with file_path <- get_data_migration_path(repo, file_name),
         true <- File.regular?(file_path),
         {{:module, module, _, _}, _} <- Code.eval_file(file_path) do
      {:ok, module}
    else
      false -> {:error, "Não foi possível encontrar a migração de dados."}
      _ -> {:error, "A migração de dados aparenta ser inválida."}
    end
  end

  defp get_data_migration_path(repo, file_name) do
    repo
    |> Ecto.Migrator.migrations_path("data_migrations")
    |> Path.join(file_name)
  end
```

Temos 2 funções auxiliares aqui:

* `get_data_migrations_path/2`: será responsável retornar o path do arquivo de migração que será executado.
* `eval_data_migration/2`: irá fazer um eval da migração, retornando uma tupla de sucesso/erro, e o módulo da migração em caso de sucesso

A nossa função `migrate_data/1` irá:
1. Garantir o carregamento da aplicação;
2. Iterar numa lista de repos;
3. Para cada repo pegar o path completo do arquivo de data migration;
4. Fazer o eval da `data_migration`;
5. Passar a função `run/1` da data migration para o Ecto.Migrator, que irá executar a migração;

### Criando uma data migration

Para criar uma data migration basta criar um arquivo no diretório `/priv/repo/data_migrations/`. Devemos dar um nome descritivo para o arquivo, como `fix_trybetunes_module.exs`.

Nossa migração só irá precisar ser um módulo simples com nossa função `run/1`:

```elixir
defmodule MyProject.Repo.DataMigrations.FixTrybetunesModule do
  def run(repo) do
    repo.update_all(
      from(p in "projects",
        where: p.template == "trybetunes",
        update: [set: [module: "frontend"]] 
      ),
      []
    )
  end
end
```

### Usando as data migrations

Com a nossa nova função `Release.migrate_data/1`, executar nossas data migrations é tão simples quanto… chamar uma função 😀

**Utilizando o IEx:**
* `iex -S mix` em ambiente de desenvolvimento;
* `./release_bin remote` em ambiente de produção ou staging, onde um deploy já foi feito, a aplicação já está sendo executada, e só temos o binário dela disponível;

```bash
iex(1)> Release.migrate_data("fix_trybetunes_module.exs")
[info] A migração de dados foi executada.
```

**Chamando a função diretamente através do binário de release:**

```bash
$ ./release_bin eval "Release.migrate_data('fix_trybetunes_module.exs')"
[info] A migração de dados foi executada.
```

### Vantagens
* Separação das responsabilidades;
* Versionamento e revisão de operações que em outro momento seriam feitas diretamente no banco de dados;
* Controle de quando será executado, e a possibilidade de executar a mesma operação quantas vezes se fizer necessário;


E é isso, implementadas as migrações de dados!