Usando database migrations em Go com Goose

Como configurar e executar database migrations em Go usando Goose

Usando database migrations em Go com Goose
Livro de Receitas

Você precisa realizar (e documentar!) alterações na estrutura do seu banco de dados de forma fácil e segura em seu projeto Go e não sabe como? Esta receita te ensina como!

Ingredientes

  • Go versão 1.22 ou posterior (vai funcionar com versões anteriores também, mas esta receita leva em consideração esta versão pra frente).
  • Goose ( https://pressly.github.io/goose/ ) a ferramenta de database migrations que usaremos nesta receita.
  • Seus scripts de atualização de banco de dados.
  • Seu gerenciador de banco de dados relacional (usaremos o PostgreSQL, mas o Goose é compatível com praticamente todas as soluções do mercado)

Passo a passo

Primeiro passo: onde seus scripts estarão guardados

O Goose pode ler seus scripts em diferentes localizações: sistema de arquivos, rede, AWS S3 ou mesmo alguma fonte customizada por você. Nesta receita iremos usar como fonte arquivos embarcados no binário gerado pelo compilador do Go em seu projeto.

A vantagem desta opção é que a implantação é mais simples: vai tudo junto. A desvantagem é que... vai tudo junto, então você precisa tomar cuidado para não terminar com binários enormes devido a estes scripts.

Mas não fique triste, dado que você pode trocar depois por outras opções de armazenamento, pra começar embarcado não é problema.

Embarcar arquivos em Go é relativamente simples. Em nosso projeto de teste, criamos um pacote chamado "migrations" e, em seu interior, um diretório chamado "scripts". O código Go que irá iniciar as migrations é exposto a seguir:

package migrations

import (
	"embed"

	"github.com/pressly/goose/v3"
)

//go:embed scripts/*.sql
var embedMigrations embed.FS

Os arquivos embarcados sempre levam em consideração o caminho relativo, então colocamos todos os nossos arquivos que terminam com a extensão ".sql" na pasta "scripts", relativa ao nosso arquivo usando o recurso "embed" do Go.

Segundo passo: seus arquivos de migrations

Seus arquivos de script devem seguir uma sintaxe bem simples na sua nomenclatura: [número da versão][nome do arquivo].sql. Exemplos: 0001_usuarios.sql , 0002_pedidos.sql, etc. Guarde todos eles no diretório "scripts", relativo ao arquivo Go que irá referenciá-los, ok?

A sintaxe dos arquivos é simples: Goose usa anotações embarcadas nos comentários para definir quando os scripts serão executados. Há várias ( https://pressly.github.io/goose/documentation/annotations/ ), mas nós só usaremos duas nesta receita: Up (que executa o nosso script) e Down (que desfaz a execução do script caso algo dê errado).

Vamos a um exemplo?

-- +goose Up

create table tenant (
    id serial,
    uuid varchar(64) not null,
    name varchar(128) not null,
    code varchar(128) not null,
    active boolean not null default true,
    created_at timestamp not null default now(),
    primary key(id)
);

-- +goose Down

drop table tenant;

De +goose Up até +goose Down o script será executado dentro da mesma mesma transação. Caso algo dê errado, o script de +goose Down pra baixo será executado.

💡
Importante: nem toda execução de modificação de estruturas de dados é realizada em uma transação. Criação de bancos de dados, por exemplo, dependendo do seu SGBD, pode não ser realizada em uma transação.

Terceiro passo: executando o Goose

O Goose pode ser executado de duas formas: como ferramenta de linha de comando (CLI) ou como biblioteca. Em nosso caso, será como uma biblioteca, executado na inicialização do nosso programa. Vamos ao código de exemplo?

package migrations

import (
	"seuprojeto/internal/data"
	"embed"

	"github.com/pressly/goose/v3"
)

//go:embed scripts/*.sql
var embedMigrations embed.FS

func RunMigrations() error {
    // obtemos o *sql.DB, que será usado pelo Goose
    // usando este código fictício do nosso projeto
	var db, errDB = data.Connect()
	if errDB != nil {
		return errDB
	}
    // definimos que o Goose carregará as migrations
    // a partir do código embarcado
	goose.SetBaseFS(embedMigrations)
    
    // definimos o dialeto (qual SGBD) vamos usar
	if err := goose.SetDialect("postgres"); err != nil {
		panic(err)
	}
    
    // finalmente: executamos as migrations passando
    // o nome do diretório, o mesmo usado em "embed"
	if err := goose.Up(db, "scripts"); err != nil {
		panic(err)
	}
    // tudo deu certo: não retornamos erro algum!
	return nil
}

O Goose armazenará o histórico de revisões de alterações no banco de dados em uma tabela chamada goose_db_version. Evite apagar ou alterar os dados desta tabela no futuro, ok?

Para aprender mais

Assustadoramente simples, hein? Claro que há diversos outros detalhes por trás do Goose que recomendo você aprender. Recomendo muito que você use então o site oficial do projeto: https://pressly.github.io/goose/

Mantido por itexto Consultoria