Como executar migrations de forma automatizada

Se você não é muito fã de ORMs como eu, um dos problemas que você mais enfrenta é o de como realizar alterações em seu banco de dados de forma segura e automática.

Nesse post, vou mostrar como fazer isso utilizando o Migrate (https://github.com/golang-migrate/migrate), um projeto open source escrito em Go para realizar migrations em bancos de dados.

Antes de começar, embora os exemplos contidos nesse tutorial serem utilizando postgres, o Migrate suporta os seguintes bancos de dados:

mongodb+srv, firebirdsql, clickhouse, cockroachdb, mongodb, mysql, sqlserver, cassandra, crdb-postgres, postgres, postgresql, spanner, stub, cockroach, neo4j, pgx, redshift, firebird.

Tendo esclarecido esse ponto, vamos iniciar um projeto com nome github.com/aprendagolang/migrate (go mod init github.com/aprendagolang/migrate) e depois escrever uma pequena API.

package main

import (
    "database/sql"
    "encoding/json"
    "net/http"
    "time"

    "github.com/aprendagolang/migrate/db"
    "github.com/go-chi/chi/v5"
)

func main() {
    conn, err := db.OpenConnection()
    if err != nil {
        panic(err)
    }

    r := chi.NewRouter()
    r.Get("/users", ListUsers(conn))

    http.ListenAndServe(":8080", r)
}

type User struct {
    ID        sql.NullInt64
    FirstName string
    LastName  string
    CreatedAt time.Time
}

func ListUsers(conn *sql.DB) http.HandlerFunc {
    return func(rw http.ResponseWriter, r *http.Request) {
        rows, err := conn.Query(`SELECT * FROM users`)
            if err != nil {
                return
            }

            var users []User

            for rows.Next() {
                var user User

                err = rows.Scan(&user.ID, &user.FirstName, &user.LastName, &user.CreatedAt)
                if err != nil {
                    continue
                }

                users = append(users, user)
            }

            rw.Header().Add("Content-Type", "application/json")
            json.NewEncoder(rw).Encode(users)
    }
}

Como o intuito aqui é apenas exemplificar a utilização e automação de migrations, o código acima foi colocado todo no arquivo main.go.

Se você reparar, um dos imports que temos ali é github.com/aprendagolang/migrate/db, que é o nosso package de banco de dados. Dentro desse package, ou seja, dessa pasta, vamos criar um arquivo com o nome connection.go.

Nesse arquivo vamos escrever uma função para realizar a conexão com o banco de dados.

package db

import (
    "database/sql"

    _ "github.com/lib/pq"
)

func OpenConnection() (*sql.DB, error) {
    conn, err := sql.Open("postgres", "host=localhost port=5432 user=gopher password=1122 dbname=foobar sslmode=disable")
    if err != nil {
        panic(err)
    }

    err = conn.Ping()
    if err != nil {
        panic(err)
    }

    return conn, err
}

Isso nos dá uma API totalmente funcional. Porém ainda nos falta a parte principal, que é a automação das migrations.

Para isso, vou criar uma nova pasta com o nome de migrations. Essa pasta vai conter todos os arquivos de UP (fazer modificações) e DOWN (desfazer modificações).

Embora possamos criar e nomear esses arquivos na mão, para seguir a convenção do Migrate sem falhas, eu recomendo instalar o CLI na sua máquina (🛠  https://github.com/golang-migrate/migrate/tree/master/cmd/migrate).

Com o CLI instalado na sua máquina, e estando dentro da pasta do projeto, execute o seguinte comando:

$ migrate create -ext sql -dir migrations -seq users

Se você olhar na pasta migrations, verá que foram criados os arquivos 000001_users.up.sql e 000001_users.down.sql.

No arquivo 000001_users.up.sql, vamos adicionar o SQL para criar nossa tabela USERS.

CREATE TABLE IF NOT EXISTS users (
  id BIGSERIAL primary key,
  first_name TEXT not null,
  last_name TEXT,
  created_at TIMESTAMP default now()
);

E como o DOWN é responsável por reverter o UP, vamos adicionar um drop da tabela USERS no arquivo 000001_users.down.sql.

DROP TABLE IF EXISTS users;

Com os dois arquivos criados, e tendo o CLI instalado na máquina, já é possível aplicar a migration utilizando o comando:

$ migrate -path ./migrations -database "postgresql://gopher:[email protected]:5432/foobar?sslmode=disable" -verbose up

E desfazer as mudanças com o comando:

$ migrate -path ./migrations -database "postgresql://gopher:[email protected]:5432/foobar?sslmode=disable" -verbose down

Embora para realizar o DOWN de forma automatizada seja um pouco complexo, já que teríamos que ter alguma espécie de controle de versão do app, o UP é simples e como o Migrate controla as versões e só aplica o que há de novo, podemos aplicar o UP sempre que a app subir.

Para fazer isso, vamos fazer uma pequena modificação na nossa função que faz conexão com banco de dados.

Primeiramente vamos importar os packages:

"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"

Agora, logo após o teste de conectividade com a funçãoo Ping(), vamos adicionar o código responsável por realizar o UP.

driver, err := postgres.WithInstance(conn, &postgres.Config{})
m, err := migrate.NewWithDatabaseInstance(
    "file://./migrations",
    "postgres", driver)
if err != nil {
    panic(err)
}
m.Up()

Ao levantar nossa API com o comando go run main.go, podemos ir até o banco de dados e verificar que a tabela users foi criada com sucesso.

Como eu disse anteriormente, para fazer o DOWN de forma automatizada depende muito do contexto de cada aplicação, e por isso não vou entrar nesse ponto. De qualquer forma, o código para fazer o DOWN seria basicamente o do exemplo anterior, apenas substituindo o m.Up() por m.Down().

Deixem suas dúvidas nos comentários.

Até a próxima!


Subscreva

Fique por dentro de tudo o que acontece no mundo Go.

Deixe uma resposta