Criando uma mix task em Elixir

Carlos Souza
January 12, 2024

Uma mix task é um script Elixir executado através do mix e normalmente utilizado para tarefas corretivas. Neste artigo, iremos escrever uma task para popular uma coluna recém criada em uma tabela no banco de dados.

Infelizmente, é comum encontrarmos aplicações com este tipo de funcionalidade como parte de uma migration. Entretanto, tal abordagem é uma anti-pattern e deve ser evitada. Migrations devem ser utilizadas única e exclusivamente para mudanças na estrutura do banco de dados, e não para a manipulação de dados.

Migrations devem ser utilizadas única e exclusivamente para mudanças na estrutura do banco de dados, e não para a manipulação de dados.

Utilizaremos como exemplo uma aplicação simples de CRUD de guitarras 🎸. O código fonte da aplicação de exemplo deste post está disponível na url a seguir:

https://github.com/idopterlabs/guitar_store/tree/mix-task

Este post é continuação do post deploy de aplicações Phoenix no Heroku. Os passos a seguir funcionam apenas com a estratégia de builpacks — eles não irão funcionar para aplicações que utilizem releases.

O Problema

No mundo dos instrumentos musicais, existe uma categoria chamada de custom shop. Modelos custom shop são unidades especiais de um instrumento, feitas com especificações únicas, materiais de melhor qualidade e produzidas em quantidade limitada. Por estes motivos, seus preços tendem a ser mais altos do que modelos feitos em maior escala 💸.

Nossa aplicação GuitarStore precisa indicar quando um modelo de guitarra é custom shop. Para isto, iremos adicionar uma coluna à tabela guitars chamada is_custom_shop. O código a seguir é a migration que cria esta nova coluna:

Alguns detalhes desta migration:

  • Um novo campo is_custom_shop é adicionado à tabela guitars.
  • O novo campo é do tipo boolean.
  • O valor padrão (default) do novo campo é false.
  • Para otimizar as queries que utilizem este campo como filtro, um index é criado para o mesmo.

Após a execução da migration, novas guitarras adicionadas ao sistema poderão ser marcadas como custom shop através de um checkbox no formulário, ilustrado a seguir:

O checkbox ajuda no cadastro de novas guitarras. Porém, o sistema encontra-se em produção e possui um número de guitarras já cadastradas. Este é um cenário bastante comum no ciclo de desenvolvimento de software. Ao desenvolvermos novas funcionalidades, mudanças no código e na estrutura do banco de dados devem sempre levar em conta os dados já existentes.

Ao desenvolvermos novas funcionalidades, mudanças no código e na estrutura do banco de dados devem sempre levar em conta os dados já existentes.

Todas as guitarras existentes no sistema terão, por padrão, o valor false para o novo campo is_custom_shop. Baseado na regra de negócio que determina se uma guitarra é custom shop, precisamos analisar as guitarras existentes no banco e atualizar a nova coluna.

Iremos classificar uma guitarra como custom shop caso ela seja de uma determinada marca, modelo e ano de fabricação. Para fins ilustrativos, estes dados estão hardcoded no módulo GuitarStore.Utils a seguir:

Um dos benefícios de mantermos o código de manipulação de dados em tasks, e separado do módulo de migrations, é o fato de podermos facilmente testá-lo! Antes do código da task, vamos olhar o código de teste:

As seguintes ações acontecem neste código de teste:

  • Duas guitarras são criadas 🎸🎸
  • Uma das guitarras contém a marca, modelo e ano de um modelo custom shop. Esta informação é retornada pela da mesma função que será utilizada pela task — Utils.custom_shop_entries()
  • Após a criação das guitarras, verificamos que o valor padrão do campo is_custom_shop é false para ambas.
  • Executamos a função principal da task — PopulateIsCustomShop.run([])
  • Lemos as duas guitarras do banco de dados para que os dados estejam atualizados, e verificamos que uma delas agora está marcada como sendo custom shop.

Agora, finalmente, o código da task:

Algumas observações sobre o código da task:

  • Os atributos de @moduledoc e @shortdoc são utilizados para documentar o propósito deste módulo e servir de documentação para o comando de help:
    mix help populate_is_custom_shop
  • Para que este comando rode com sucesso, este módulo precisa estar compilado. Em caso de dúvidas, compile a aplicação toda com o comando mix compile.
  • A função principal run/1 utiliza Enum.map/2 para iterar sobre os dados de modelos custom shop retornados pela função Utils.custom_shop_entries() e, a cada loop, os passa para a função update_guitars/1.
  • A função update_guitars/1 utiliza pattern matching para extrair marca, modelo e ano da tuple passada como argumento, e os utiliza como filtro para uma query na tabela guitars.
  • O resultado da query é passado como argumento para a função Repo.update_all/2, que atualiza o campo is_custom_shop para true.

Para rodar a task localmente, executamos o comando a seguir:

mix populate_is_custom_shop

Para rodar a task no Heroku, passamos este comando como argumento para o comando heroku run, como no código a seguir:

heroku run "POOL_SIZE=2 mix populate_is_custom_shop"

Pronto! Concluímos nossa mix task, testada, e que popula uma coluna de banco de dados seguindo uma regra de negócio 💥

Conclusão

Neste artigo, vimos como escrever, testar e executar uma mix task que atualiza uma tabela no banco de dados. Aprendemos que migrations devem ser utilizadas única e exclusivamente para mudanças na estrutura do banco de dados, e não para a manipulação de dados.

Agora você está pronto para escrever mix tasks e executá-las localmente e também no Heroku!

E aí, o que achou ? Caso tenha alguma consideração ou dica, deixe seu comentário! :)

A Idopter Labs é uma empresa de consultoria em desenvolvimento de software com o objetivo de transformar idéias em negócios digitais rentáveis. Prezamos por transparência, objetividade e agilidade. Estamos no mercado desde 2016 e, desde então, ajudamos diversos clientes em suas iniciativas nas mais diferentes indústrias. Adotamos Elixir em nossa stack por sua performance, clareza e pela excelente comunidade que existe ao seu redor.