Wiliam V. Joaquim

Wiliam V. Joaquim

Usando migrations com Golang

thumbnail

O que são migrations?

As migrations são muito utilizadas para manter um controle de versão da estrutura do seu banco de dados, é uma solução muito útil para manter seu banco de dados organizado.

Imagine que você tenha uma tabela de usuários e precise inserir um novo campo nessa tabela, sem migrations você precisaria rodar um SQL na mão, como por exemplo:

  ALTER TABLE "users" ADD COLUMN "phone" VARCHAR(255) NULL;

Agora, toda vez que precisar recriar a tabela users, você vai precisar lembrar de criar este campo, a menos que alterar a criação original da tabela users, porém isso começa a fica inviável a medida que sua tabela e sua aplicação crescem, por isso utilizar as migrations são uma ótima opção.

As migrations

O funcionamento das migrations são relativamente simples, geralmente temos um arquivo de up e outro de down, alguns ORMs como o PrismaORM criam apenas 1 arquivo, no arquivo de up criamos nosso SQL que vai criar ou alterar nosso banco, no arquivo de down criamos o SQL que desfaz a alteração.

Qual a vantagem?

Agora com esses arquivos, mantemos um histórico de alterações do banco de dados, cada alteração tem seu arquivo de up e down, agora se precisarmos criar as tabelas, rodamos todos os arquivo de up e tudo é criado, se precisar reverter, basta rodar o down.

Migrations em Go

O Go não oferece nativamente suporte ao uso de migrations, mas poderíamos utilizar o ORM que tenha essa funcionalidade, como o GORM que é o mais utilizado pela comunidade, mas podemos utilizar as migrations sem o uso de um ORM, para isso vamos utilizar o pacote golang-migrate.

Pacote Golang Migrate

O pacote golang-migrate é o mais recomendado para isso, já temos tudo que precisamos para gerenciar nossas migrations e oferece suporte a praticamente todos os bancos de dados, para o nosso exemplo vamos utilizar o PostgreSQL.

Nosso projeto de exemplo

Já criei previamente um projeto simples, mas vou explicar rapidamente, pois o foco é utilizar as migrations.

Project structure

Teremos essa estrutura, bem simples, o código para exemplo deve ficar inteiro em main.go:

  package main

  import (
    "database/sql"
    "fmt"
    "log"
    "os"

    "github.com/joho/godotenv"
    _ "github.com/lib/pq"
  )

  func main() {
    // load .env file
    godotenv.Load()

    postgresURI := os.Getenv("DATABASE_URL")
    db, err := sql.Open("postgres", postgresURI)
    if err != nil {
      log.Panic(err)
    }
    err = db.Ping()
    if err != nil {
      db.Close()
      log.Panic(err)
    }

    fmt.Println("Connected to database")

    // keep the program running
    select {}
  }

Utilizando o golang-migrate

Precisamos instalar a CLI do pacote golang-migrate, veja como instalar aqui, rode o comando:

  migrate -version

Se a saida for algo como:

  v4.16.2

Todo certo para continuar! O próximo passo é criar nossa primeira migrations com o comando:

  migrate create -ext=sql -dir=internal/database/migrations -seq init
  • ext: determina a extensão, vamos usar o sql.
  • dir: Aqui fica o diretório onde vai ser criado as nossas migrations.
  • seq: Determina a sequência do nome do arquivo da migrations, vamos usar numérico, pode ser usado timestamp.

Com isso, você vai perceber que foi criado uma pasta chamada migrations dentro da pasta database.

Project Migrations

Foi criado o arquivo up e o arquivo down, na sequência, como é a primeira fica 000001, se rodar novamente o comando migrate create, vai criar a migration 000002. Agora vamos criar nosso SQL e rodar as migrations:

No arquivo de up, vamos criar a seguinte tabela:

  CREATE TABLE users (
    id VARCHAR(36) NOT NULL PRIMARY KEY,
    first_name VARCHAR(255) NOT NULL,
    last_name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP(3) NOT NULL
  );

Agora no arquivo de down vamos remover a tabela:

  DROP TABLE IF EXISTS users;

Com nosso SQL pronto, podemos rodar o up da nossa migrations, não se esqueça de garantir que seu banco esteja rodando, para isso deixei no projeto um arquivo docker compose para rodar uma imagem do PostgreSQL.

  migrate -path=internal/database/migrations -database "postgresql://golang_migrate:golang_migrate@localhost:5432/golang_migrate?sslmode=disable" -verbose up
  • path: Informa onde estão as nossas migrations.
  • database: Url da conexão com o banco de dados.
  • -verbose: Apenas para exibir todas as execuções.

Se acessarmos algum cliente como o PgAdmin ou Beekeeper, ou acessando seu container via bash e verificando via CLI, poderemos ver que a tabela foi criada com sucesso:

Beekeeper

Agora podemos rodar o down, é exatamente o mesmo comando, porém alterando de up para down:

  migrate -path=internal/database/migrations -database "postgresql://golang_migrate:golang_migrate@localhost:5432/golang_migrate?sslmode=disable" -verbose down

Com isso a tabela é removida.

Adicionando mais campos

Vamos ver agora como ficaria se fosse necessário adicionar mais um campo na tabela users, sem migrations, teríamos que alterar diretamente a tabela original, mas com migrations não precisamos, vamos criar outra migration:

  migrate create -ext=sql -dir=internal/database/migrations -seq init

Vai criar a migration up e down com sequencial 000002, vamos adicionar o campo phone:

up:

  ALTER TABLE "users" ADD COLUMN "phone" VARCHAR(255) NULL;

down:

  ALTER TABLE "users" DROP COLUMN "phone";

Ao rodar novamente:

  migrate -path=internal/database/migrations -database "postgresql://golang_migrate:golang_migrate@localhost:5432/golang_migrate?sslmode=disable" -verbose up

Nosso campo phone é adicionado a tabela user, mas e se eu quiser fazer o down apenas na migration que adiciona o campo phone? É possível, basta usar o mesmo comando, passando o valor 1, que significa que deseja desfazer a última migration:

  migrate -path=internal/database/migrations -database "postgresql://golang_migrate:golang_migrate@localhost:5432/golang_migrate?sslmode=disable" -verbose down 1

O campo phone é removido.

Facilitando o uso da CLI

Como você pode perceber, os comandos do golang-migrate podem ser um pouco cansativos de usar, podemos facilitar usando um arquivo makefile.

  include .env

  create_migration:
    migrate create -ext=sql -dir=internal/database/migrations -seq init

  migrate_up:
    migrate -path=internal/database/migrations -database "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?sslmode=disable" -verbose up

  migrate_down:
    migrate -path=internal/database/migrations -database "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?sslmode=disable" -verbose down

  .PHONY: create_migration migrate_up migrate_down

Criamos atalhos que rodam o comando de migrate, precisamos incluir nossas envs usando o include .env, depois criamos os comandos:

  • create_migration: Cria nossos arquivos de migration.
  • migrate_up: Executa nossas migrations up.
  • migrate_down: Executa nossas migrations down.
  • PHONY: Garante que vai executar um comando, o makefile pode tentar pegar um arquivo, caso existe arquivos com o nome migrate_up por exemplo.

Com isso, basta usar o comando:

  make create_migration

E teremos nossos arquivos de migration criados, isso vale para os demais atalhos criados.

Considerações finais

Nesse post vimos como utilizar as migrations e o quanto elas são importantes para manter um histórico de alterações e facilitar a manutenção do nosso banco de dados. Mas vale ressaltar que o uso de forma errada das migrations podem acabar fazendo você perder os dados do seu banco, por isso é importante ter esse conhecimento de como funcionam as migrations e como seu ORM ou sua linguagem lidam com as migrations.

Evite apagar migrations, elas podem se tornar um pesadelo se não forem utilizadas de forma correta.

repositório do projeto

Gopher credits