Pular para o conteúdo

Trilha de Go · parte 20 de 37

SQLC: SQL tipado a partir de queries em Go

Como o SQLC gera código Go fortemente tipado a partir de queries SQL puras, com a interface Querier e sem reflexão em tempo de execução.

Publicada em · 9 min de leitura

Tamanho da fonte:
Largura da coluna:

Ao projetar a camada de acesso a dados em Go, os desenvolvedores costumam ser forçados a escolher entre dois extremos problemáticos:

  1. Usar um ORM tradicional (como GORM ou Hibernate): Esconde o SQL atrás de abstrações opacas, depende de reflexão dinâmica pesada em tempo de execução, dificulta a depuração de planos de execução (EXPLAIN ANALYZE) e torna quase inevitável a introdução acidental do temido problema de consultas excessivas N+1.
  2. Escrever Go puro (database/sql ou pgx manual): Garante controle absoluto sobre as queries, mas exige centenas de linhas de código repetitivo de rows.Scan(&a, &b, &c...). Além disso, qualquer erro de digitação no nome de uma coluna ou tipo incompatível só é descoberto em produção quando a query falha.

Para resolver esse dilema e unir o melhor dos dois mundos, surgiu o SQLC (sqlc.dev), uma das ferramentas mais aclamadas e adotadas pela comunidade moderna de Go.

A premissa do SQLC é simples e revolucionária: Você escreve o SQL puro; o SQLC gera código Go 100% tipado, seguro em tempo de compilação e com zero reflexão.

Esta aula disseca o funcionamento do compilador do SQLC, as anotações semânticas de queries, a configuração moderna com pgx/v5 e como a interface Querier gerada automaticamente simplifica suítes de testes unitários.


O Mapa da Aula

ParteSeçãoO que você vai dominar
1. Filosofia1 e 2O dilema ORM vs Go Puro e por que o SQLC compila SQL com segurança estática
2. O Compilador por Dentro3 e 4O parser AST nativo do PostgreSQL em tempo de desenvolvimento e a geração de código
3. Anotações de Queries5 e 6As diretivas essenciais: :one, :many, :exec e :execresult
4. Configuração Moderna7 e 8Configurando sqlc.yaml para pgx/v5, tags JSON automáticas e a interface Querier
5. Nulidade e Tipos9Tratamento de campos nulos com pgtype e sobrescrita de tipos (Type Overrides)
6. Código de Produção10 e 11Exemplo completo de ponta a ponta, mocking trivial em testes e checklist de qualidade

Pré-requisitos recomendados: database-sql e Connection Pooling, PostgreSQL com pgx e Mocks e Dublês de Teste.


Parte 1: O Dilema da Persistência em Go

1. Por que a comunidade Go rejeita ORMs tradicionais?

Em ecossistemas como Java (Hibernate) ou Ruby (ActiveRecord), os ORMs tentam mapear tabelas de banco diretamente para classes orientadas a objetos. No Go, esse paradigma entra em conflito direto com a filosofia de simplicidade e clareza da linguagem:

comparativo orm vs sqlc
figura 1 · comparativo orm vs sqlc
  • Reflexão Pesada: ORMs dependem intensamente do pacote reflect para inspecionar campos de structs em tempo de execução, consumindo CPU desnecessária e alocando memória no Heap.
  • Complexidade Oculta: Escrever joins complexos, queries analíticas ou CTEs (Common Table Expressions) em DSLs de ORM costuma gerar códigos incompreensíveis. O desenvolvedor passa mais tempo aprendendo a sintaxe proprietária do ORM do que usando SQL.
  • A Armadilha do N+1: ORMs com carregamento preguiçoso (lazy loading) mascaram queries adicionais dentro de laços de repetição, degradando a performance silenciosamente.

2. A Filosofia do SQLC

O SQLC inverte essa lógica através de uma abordagem orientada a SQL (SQL-First):

  • Você continua sendo o mestre absoluto do seu banco de dados: você cria as tabelas com CREATE TABLE e escreve suas consultas em arquivos .sql puros.
  • O SQLC lê suas queries, valida a sintaxe diretamente contra o motor do banco e gera structs Go correspondentes e métodos de repositório otimizados.
  • O código Go gerado utiliza chamadas diretas de QueryRow e Scan compiladas estaticamente, com zero overhead de reflexão e desempenho idêntico a código escrito manualmente pelos engenheiros mais experientes.

Parte 2: Como o SQLC Opera Sob o Capô

3. O Parser Nativo do PostgreSQL em Tempo de Desenvolvimento

O maior diferencial do SQLC em relação a geradores de código ingênuos é que ele não usa expressões regulares para tentar adivinhar seu SQL:

pipeline compilador sqlc
figura 2 · pipeline compilador sqlc

O compilador do SQLC embute o próprio código-fonte do parser léxico e analisador sintático (AST) oficial do PostgreSQL:

  1. Ele carrega seus arquivos de schema (schema.sql) e monta a árvore de catálogo de tabelas, colunas, tipos e restrições.
  2. Ao ler sua query (query.sql), ele valida se as colunas selecionadas realmente existem, se as tabelas foram declaradas e se os parâmetros $1, $2 são compatíveis com os tipos das colunas no banco.
  3. Se você errar uma vírgula ou tentar passar uma string para um campo INTEGER, o comando sqlc generate falha imediatamente no terminal com o número exato da linha do erro, impedindo que código quebrado chegue a ser compilado em Go!

Parte 3: Anotações de Queries no SQLC

Nos seus arquivos .sql, você instrui o SQLC sobre como cada consulta deve ser transformada em função Go através de um comentário estruturado logo acima da query:

-- name: <NomeDaFuncao> <ModificadorDeRetorno>

Existem 4 modificadores fundamentais:

ModificadorO que faz no GoAssinatura GeradaUso Típico
:oneRetorna exatamente 1 registro ou erro(T, error)Busca por ID, e-mail único ou INSERT ... RETURNING
:manyRetorna um slice de registros([]T, error)Listagens com WHERE, paginação e filtros
:execExecuta a query e descarta o retornoerrorUPDATE, DELETE ou INSERT simples sem retorno
:execresultRetorna metadados de execução(pgconn.CommandTag, error)Quando você precisa checar RowsAffected()

Parte 4: Configuração Moderna com sqlc.yaml

A partir da versão 2, o SQLC é configurado através de um arquivo sqlc.yaml na raiz do projeto.

Para projetos modernos de alto desempenho utilizando PostgreSQL com pgx, configuramos o backend nativo pgx/v5:

version: "2"
sql:
  - engine: "postgresql"
    schema: "sql/schema.sql"       # Arquivo ou pasta com DDLs (CREATE TABLE)
    queries: "sql/queries.sql"     # Arquivo ou pasta com queries DML
    gen:
      go:
        package: "db"              # Nome do pacote Go gerado
        out: "internal/db"         # Pasta de destino do código Go
        sql_package: "pgx/v5"      # Gera código nativo para pgx v5!
        emit_json_tags: true       # Adiciona tags `json:"nome"` nas structs
        emit_interface: true       # Gera a interface Querier (crucial para testes!)
        emit_empty_slices: true    # Retorna []T vazio em vez de nil quando não há linhas

4. O Superpoder do emit_interface: true

Ao ativar a opção emit_interface: true, o SQLC gera automaticamente um arquivo querier.go contendo uma interface pública com todos os métodos das suas queries:

// Código gerado automaticamente pelo sqlc:
type Querier interface {
    CriarUsuario(ctx context.Context, arg CriarUsuarioParams) (Usuario, error)
    ObterUsuarioPorID(ctx context.Context, id int64) (Usuario, error)
    ListarUsuariosAtivos(ctx context.Context) ([]Usuario, error)
    AtualizarBio(ctx context.Context, arg AtualizarBioParams) error
    DeletarUsuario(ctx context.Context, id int64) error
}

Isso significa que na sua camada de regras de negócio (Clean Architecture em Go), seu Use Case depende apenas da interface db.Querier. Em testes unitários, você pode mockar o banco de dados em segundos usando Mocks e Dublês de Teste sem precisar de contêineres Docker nem dependências externas!


Parte 5: Tratamento de Nulidade e Mapeamento de Tipos

O SQLC mapeia os tipos com precisão cirúrgica baseado nas restrições de nulidade do schema:

CREATE TABLE usuarios (
    id         BIGSERIAL PRIMARY KEY,       -- int64
    nome       VARCHAR(100) NOT NULL,       -- string pura
    bio        TEXT,                        -- pgtype.Text (anulável)
    idade      INTEGER,                     -- pgtype.Int4 (anulável)
    created_at TIMESTAMP NOT NULL           -- pgtype.Timestamp
);
  • Campos NOT NULL: Mapeiam diretamente para os tipos primitivos correspondentes do Go (string, int64, bool).
  • Campos Anuláveis (Sem NOT NULL): Mapeiam para structs que suportam valores nulos sem causar pânico de ponteiro, como os tipos nativos do pgx (pgtype.Text{String: "...", Valid: true}).

Parte 6: Código Prático Completo de Produção

Vamos criar um módulo completo de gerenciamento de usuários demonstrando o fluxo: DDL $\to$ Queries $\to$ Geração $\to$ Consumo na Aplicação.

1. O Schema DDL (sql/schema.sql):

CREATE TABLE usuarios (
    id BIGSERIAL PRIMARY KEY,
    nome VARCHAR(100) NOT NULL,
    email VARCHAR(150) UNIQUE NOT NULL,
    bio TEXT,
    ativo BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMP NOT NULL DEFAULT now()
);

2. As Consultas SQL (sql/queries.sql):

-- name: ObterUsuarioPorID :one
SELECT id, nome, email, bio, ativo, created_at
FROM usuarios
WHERE id = $1 LIMIT 1;

-- name: ListarUsuariosAtivos :many
SELECT id, nome, email, bio, ativo, created_at
FROM usuarios
WHERE ativo = true
ORDER BY nome ASC;

-- name: CriarUsuario :one
INSERT INTO usuarios (nome, email, bio)
VALUES ($1, $2, $3)
RETURNING id, nome, email, bio, ativo, created_at;

-- name: AtualizarBio :exec
UPDATE usuarios
SET bio = $2
WHERE id = $1;

-- name: DeletarUsuario :exec
DELETE FROM usuarios
WHERE id = $1;

3. Gerando o Código Go:

No terminal:

sqlc generate

O SQLC gera instantaneamente a pasta internal/db/ contendo:

  • models.go: Struct Usuario com todos os tipos tipados e tags JSON.
  • querier.go: Interface Querier.
  • query.sql.go: Todos os métodos executáveis implementando queries nativas com pgx/v5.

4. Consumindo no Código de Aplicação (main.go):

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"

	"meu-projeto/internal/db"
)

func main() {
	ctx := context.Background()

	// 1. Inicializa o pool de conexões do pgx
	pool, err := pgxpool.New(ctx, "postgres://usuario:senha@localhost:5432/meubanco")
	if err != nil {
		log.Fatalf("falha ao conectar no postgres: %v", err)
	}
	defer pool.Close()

	// 2. Instancia o conjunto de queries gerado pelo sqlc
	queries := db.New(pool)

	// 3. Execução type-safe: Criar Usuário
	novoUser, err := queries.CriarUsuario(ctx, db.CriarUsuarioParams{
		Nome:  "Camila Rocha",
		Email: "camila@empresa.com",
		Bio:   pgtype.Text{String: "Engenheira de Confiabilidade", Valid: true},
	})
	if err != nil {
		log.Fatalf("falha ao criar usuário: %v", err)
	}

	fmt.Printf("Usuário criado com sucesso! ID: %d, Criado em: %v\n", novoUser.ID, novoUser.CreatedAt)

	// 4. Execução type-safe: Listar Usuários
	usuarios, err := queries.ListarUsuariosAtivos(ctx)
	if err != nil {
		log.Fatalf("falha ao listar: %v", err)
	}

	for _, u := range usuarios {
		fmt.Printf("• %s (%s) - Ativo? %v\n", u.Nome, u.Email, u.Ativo)
	}
}

Parte 7: Mockando o Banco em Testes Unitários com Querier

Como ativamos emit_interface: true, criar testes unitários para a camada de serviço se torna trivial. Você não precisa conectar em nenhum banco Docker:

package servico_test

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"

	"meu-projeto/internal/db"
)

// MockQuerier implementa a interface db.Querier gerada pelo sqlc
type MockQuerier struct {
	mock.Mock
	db.Querier
}

func (m *MockQuerier) ObterUsuarioPorID(ctx context.Context, id int64) (db.Usuario, error) {
	args := m.Called(ctx, id)
	return args.Get(0).(db.Usuario), args.Error(1)
}

func TestBuscarUsuario(t *testing.T) {
	mockDB := new(MockQuerier)

	usuarioEsperado := db.Usuario{
		ID:    42,
		Nome:  "Camila Rocha",
		Email: "camila@empresa.com",
		Ativo: true,
	}

	// Programa o mock
	mockDB.On("ObterUsuarioPorID", mock.Anything, int64(42)).Return(usuarioEsperado, nil).Once()

	// Executa a lógica de serviço
	u, err := mockDB.ObterUsuarioPorID(context.Background(), 42)

	assert.NoError(t, err)
	assert.Equal(t, "Camila Rocha", u.Nome)
	mockDB.AssertExpectations(t)
}

Parte 8: Checklist de Boas Práticas com SQLC

  1. Sempre use sql_package: "pgx/v5": Aproveite o protocolo binário e o pool nativo de alta vazão sem o overhead do database/sql.
  2. Ative sempre emit_interface: true: Permite injetar e mockar a interface db.Querier em testes unitários puros com extrema rapidez.
  3. Mantenha schema.sql e migrações sincronizados: O SQLC depende do schema DDL para saber quais colunas existem. Mantenha os arquivos DDL versionados no Git junto com suas migrações.
  4. Use nomes de queries expressivos: Prefira nomes de domínio claros como -- name: BuscarClientePorCPF :one em vez de nomes genéricos como -- name: Select :one.
  5. Integre com o comando go generate: Adicione //go:generate sqlc generate no topo do seu main.go para regenerar queries facilmente com go generate ./....

Exercício Prático Proposto

  1. Escreva um schema para um sistema de e-commerce com tabelas produtos e categorias.
  2. Crie queries usando SQLC:
    • Uma query com -- name: ListarProdutosPorCategoria :many com paginação (LIMIT $1 OFFSET $2).
    • Uma query de atualização com -- name: DecrementarEstoque :execresult.
  3. Gere o código com sqlc generate.
  4. Inspecione a struct gerada em models.go e verifique os tipos e tags JSON automáticas.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?