ORM — Object-Relational Mapper — é uma camada que mapeia structs Go para tabelas de banco de dados, gerando SQL automaticamente. O GORM é o ORM mais popular do ecossistema Go, com suporte a MySQL, PostgreSQL, SQLite e SQL Server.
A decisão de usar ORM versus SQL puro é uma das mais debatidas em desenvolvimento backend. ORMs aceleram o desenvolvimento inicial, eliminam SQL repetitivo e facilitam migrações. Em contrapartida, podem gerar queries ineficientes, obscurecer o que acontece no banco e tornar problemas de performance difíceis de diagnosticar.
A posição pragmática: use GORM para CRUD simples e migrações, escreva SQL explícito para queries complexas e de alta performance. O GORM não impede isso — permite executar SQL bruto a qualquer momento.
Instalação
go get gorm.io/gorm
go get gorm.io/driver/postgres # PostgreSQL
go get gorm.io/driver/mysql # MySQL
go get gorm.io/driver/sqlite # SQLite
Conexão e configuração
package main
import (
"fmt"
"log"
"os"
"time"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func abrirPostgres(dsn string) (*gorm.DB, error) {
config := &gorm.Config{
Logger: logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Warn, // loga apenas queries lentas
IgnoreRecordNotFoundError: true,
Colorful: true,
},
),
NowFunc: func() time.Time {
return time.Now().UTC()
},
PrepareStmt: true, // cache de prepared statements
}
db, err := gorm.Open(postgres.Open(dsn), config)
if err != nil {
return nil, fmt.Errorf("abrir postgres: %w", err)
}
// Configurar o pool de conexões subjacente
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(5 * time.Minute)
return db, nil
}
func abrirSQLite(caminho string) (*gorm.DB, error) {
return gorm.Open(sqlite.Open(caminho), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
}
Modelos: a base do GORM
import (
"time"
"gorm.io/gorm"
)
// gorm.Model inclui ID, CreatedAt, UpdatedAt e DeletedAt (soft delete)
type Categoria struct {
gorm.Model
Nome string `gorm:"size:100;not null;uniqueIndex"`
Slug string `gorm:"size:100;not null;uniqueIndex"`
}
type Produto struct {
gorm.Model
CategoriaID uint `gorm:"not null;index"`
Categoria Categoria `gorm:"foreignKey:CategoriaID"`
Nome string `gorm:"size:200;not null;index"`
Descricao string `gorm:"type:text"`
Preco float64 `gorm:"type:decimal(10,2);not null"`
Estoque int `gorm:"not null;default:0"`
Ativo bool `gorm:"not null;default:true;index"`
Tags []Tag `gorm:"many2many:produto_tags"`
}
type Tag struct {
gorm.Model
Nome string `gorm:"size:50;not null;uniqueIndex"`
Produtos []Produto `gorm:"many2many:produto_tags"`
}
type Usuario struct {
gorm.Model
Nome string `gorm:"size:200;not null"`
Email string `gorm:"size:200;not null;uniqueIndex"`
Senha string `gorm:"size:255;not null"`
Ativo bool `gorm:"not null;default:true"`
Pedidos []Pedido `gorm:"foreignKey:UsuarioID"`
}
type Pedido struct {
gorm.Model
UsuarioID uint `gorm:"not null;index"`
Usuario Usuario `gorm:"foreignKey:UsuarioID"`
Total float64 `gorm:"type:decimal(10,2);not null;default:0"`
Status string `gorm:"size:50;not null;default:'pendente'"`
ItensPedido []ItemPedido `gorm:"foreignKey:PedidoID"`
}
type ItemPedido struct {
gorm.Model
PedidoID uint `gorm:"not null;index"`
ProdutoID uint `gorm:"not null;index"`
Produto Produto `gorm:"foreignKey:ProdutoID"`
Quantidade int `gorm:"not null"`
PrecoUnit float64 `gorm:"type:decimal(10,2);not null"`
}
AutoMigrate: migrações automáticas
func executarMigracoes(db *gorm.DB) error {
return db.AutoMigrate(
&Categoria{},
&Tag{},
&Produto{},
&Usuario{},
&Pedido{},
&ItemPedido{},
)
}
AutoMigrate cria tabelas que não existem, adiciona colunas faltantes e cria índices. Ele nunca remove colunas ou altera tipos existentes — é seguro para produção. Para mudanças destrutivas, use migrações manuais com golang-migrate.
CRUD básico
package main
import (
"errors"
"fmt"
"gorm.io/gorm"
)
// CREATE
func criarProduto(db *gorm.DB, input ProdutoInput) (*Produto, error) {
produto := Produto{
CategoriaID: input.CategoriaID,
Nome: input.Nome,
Descricao: input.Descricao,
Preco: input.Preco,
Estoque: input.Estoque,
}
if err := db.Create(&produto).Error; err != nil {
return nil, fmt.Errorf("criar produto: %w", err)
}
return &produto, nil
}
// READ — por ID
func buscarProdutoPorID(db *gorm.DB, id uint) (*Produto, error) {
var produto Produto
err := db.First(&produto, id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("produto %d não encontrado", id)
}
if err != nil {
return nil, fmt.Errorf("buscar produto: %w", err)
}
return &produto, nil
}
// READ — com preload de relacionamentos
func buscarProdutoCompleto(db *gorm.DB, id uint) (*Produto, error) {
var produto Produto
err := db.
Preload("Categoria").
Preload("Tags").
First(&produto, id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("produto %d não encontrado", id)
}
return &produto, err
}
// UPDATE — apenas campos alterados
func atualizarProduto(db *gorm.DB, id uint, campos map[string]any) error {
resultado := db.Model(&Produto{}).Where("id = ?", id).Updates(campos)
if resultado.Error != nil {
return fmt.Errorf("atualizar produto: %w", resultado.Error)
}
if resultado.RowsAffected == 0 {
return fmt.Errorf("produto %d não encontrado", id)
}
return nil
}
// DELETE — soft delete (marca deleted_at, não remove fisicamente)
func deletarProduto(db *gorm.DB, id uint) error {
resultado := db.Delete(&Produto{}, id)
if resultado.Error != nil {
return fmt.Errorf("deletar produto: %w", resultado.Error)
}
if resultado.RowsAffected == 0 {
return fmt.Errorf("produto %d não encontrado", id)
}
return nil
}
// DELETE permanente — ignora soft delete
func deletarProdutoPermanente(db *gorm.DB, id uint) error {
return db.Unscoped().Delete(&Produto{}, id).Error
}
Consultas avançadas
func listarProdutosAvancado(db *gorm.DB, f FiltroProduto) ([]Produto, int64, error) {
var produtos []Produto
var total int64
query := db.Model(&Produto{})
// Filtros condicionais
if f.CategoriaID > 0 {
query = query.Where("categoria_id = ?", f.CategoriaID)
}
if f.ApenasAtivo {
query = query.Where("ativo = ?", true)
}
if f.PrecoMin > 0 {
query = query.Where("preco >= ?", f.PrecoMin)
}
if f.PrecoMax > 0 {
query = query.Where("preco <= ?", f.PrecoMax)
}
if f.Busca != "" {
query = query.Where("nome ILIKE ?", "%"+f.Busca+"%")
}
// Conta o total antes da paginação
query.Count(&total)
// Aplica paginação e ordenação
limite := f.Limite
if limite <= 0 {
limite = 20
}
err := query.
Preload("Categoria").
Preload("Tags").
Order("nome ASC").
Limit(limite).
Offset((f.Pagina - 1) * limite).
Find(&produtos).Error
return produtos, total, err
}
// Scopes: encapsulando condições reutilizáveis
func ScopeAtivo(db *gorm.DB) *gorm.DB {
return db.Where("ativo = ?", true)
}
func ScopeComEstoque(db *gorm.DB) *gorm.DB {
return db.Where("estoque > 0")
}
func ScopeCategoria(id uint) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("categoria_id = ?", id)
}
}
func produtosDisponiveisNaCategoria(db *gorm.DB, categoriaID uint) ([]Produto, error) {
var produtos []Produto
err := db.
Scopes(ScopeAtivo, ScopeComEstoque, ScopeCategoria(categoriaID)).
Order("preco ASC").
Find(&produtos).Error
return produtos, err
}
Relacionamentos: has many, belongs to, many2many
// Has Many: Um usuário tem muitos pedidos
func pedidosDoUsuario(db *gorm.DB, usuarioID uint) ([]Pedido, error) {
var pedidos []Pedido
err := db.
Where("usuario_id = ?", usuarioID).
Preload("ItensPedido.Produto").
Order("created_at DESC").
Find(&pedidos).Error
return pedidos, err
}
// Many to Many: associando tags a produto
func associarTags(db *gorm.DB, produtoID uint, nomeTags []string) error {
var produto Produto
if err := db.First(&produto, produtoID).Error; err != nil {
return err
}
var tags []Tag
for _, nome := range nomeTags {
var tag Tag
// FirstOrCreate: busca ou cria se não existir
db.FirstOrCreate(&tag, Tag{Nome: nome})
tags = append(tags, tag)
}
// Association replaces existing tags
return db.Model(&produto).Association("Tags").Replace(tags)
}
// Contando associações
func contagemPorCategoria(db *gorm.DB) ([]map[string]any, error) {
var resultados []map[string]any
err := db.Model(&Produto{}).
Select("categorias.nome as categoria, count(produtos.id) as total").
Joins("JOIN categorias ON categorias.id = produtos.categoria_id").
Where("produtos.ativo = ?", true).
Group("categorias.id, categorias.nome").
Order("total DESC").
Scan(&resultados).Error
return resultados, err
}
Transações no GORM
func criarPedidoCompleto(db *gorm.DB, usuarioID uint, itens []ItemInput) (*Pedido, error) {
var pedido Pedido
err := db.Transaction(func(tx *gorm.DB) error {
// Cria o pedido
pedido = Pedido{
UsuarioID: usuarioID,
Status: "confirmado",
}
if err := tx.Create(&pedido).Error; err != nil {
return fmt.Errorf("criar pedido: %w", err)
}
var totalPedido float64
for _, item := range itens {
// Busca produto e bloqueia para atualização
var produto Produto
err := tx.Set("gorm:query_option", "FOR UPDATE").
First(&produto, item.ProdutoID).Error
if err != nil {
return fmt.Errorf("buscar produto %d: %w", item.ProdutoID, err)
}
if produto.Estoque < item.Quantidade {
return fmt.Errorf("estoque insuficiente para %s", produto.Nome)
}
// Cria item do pedido
itemPedido := ItemPedido{
PedidoID: pedido.ID,
ProdutoID: produto.ID,
Quantidade: item.Quantidade,
PrecoUnit: produto.Preco,
}
if err := tx.Create(&itemPedido).Error; err != nil {
return fmt.Errorf("criar item: %w", err)
}
// Atualiza estoque
if err := tx.Model(&produto).
Update("estoque", gorm.Expr("estoque - ?", item.Quantidade)).Error; err != nil {
return fmt.Errorf("atualizar estoque: %w", err)
}
totalPedido += produto.Preco * float64(item.Quantidade)
}
// Atualiza total do pedido
return tx.Model(&pedido).Update("total", totalPedido).Error
})
if err != nil {
return nil, err
}
return &pedido, nil
}
SQL bruto quando necessário
// Raw SQL para queries complexas
func relatorioVendas(db *gorm.DB, mes, ano int) ([]map[string]any, error) {
var resultados []map[string]any
err := db.Raw(`
SELECT
c.nome AS categoria,
COUNT(DISTINCT p.id) AS total_pedidos,
SUM(ip.quantidade) AS unidades_vendidas,
SUM(ip.quantidade * ip.preco_unit) AS receita_total,
AVG(ip.preco_unit) AS ticket_medio
FROM itens_pedido ip
JOIN pedidos p ON p.id = ip.pedido_id
JOIN produtos pr ON pr.id = ip.produto_id
JOIN categorias c ON c.id = pr.categoria_id
WHERE
EXTRACT(MONTH FROM p.created_at) = ?
AND EXTRACT(YEAR FROM p.created_at) = ?
AND p.status != 'cancelado'
AND p.deleted_at IS NULL
GROUP BY c.id, c.nome
ORDER BY receita_total DESC
`, mes, ano).Scan(&resultados).Error
return resultados, err
}
// Exec para statements sem retorno
func desativarProdutosSemEstoque(db *gorm.DB) (int64, error) {
resultado := db.Exec(
"UPDATE produtos SET ativo = false WHERE estoque = 0 AND ativo = true")
return resultado.RowsAffected, resultado.Error
}
Hooks: lógica antes e depois de operações
// BeforeCreate é chamado automaticamente antes de cada INSERT
func (p *Produto) BeforeCreate(tx *gorm.DB) error {
if p.Preco <= 0 {
return fmt.Errorf("preço deve ser positivo")
}
if p.Nome == "" {
return fmt.Errorf("nome é obrigatório")
}
return nil
}
// AfterCreate é chamado após cada INSERT bem-sucedido
func (p *Produto) AfterCreate(tx *gorm.DB) error {
// Poderia disparar evento, invalidar cache, etc.
fmt.Printf("Produto criado: ID=%d Nome=%s\n", p.ID, p.Nome)
return nil
}
// BeforeDelete previne deleção de produtos com pedidos
func (p *Produto) BeforeDelete(tx *gorm.DB) error {
var count int64
tx.Model(&ItemPedido{}).Where("produto_id = ?", p.ID).Count(&count)
if count > 0 {
return fmt.Errorf("produto possui %d pedidos — não pode ser excluído", count)
}
return nil
}
Testes com GORM e SQLite em memória
package main
import (
"testing"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func configurarDBTeste(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("abrir DB de teste: %v", err)
}
if err := db.AutoMigrate(&Categoria{}, &Tag{}, &Produto{},
&Usuario{}, &Pedido{}, &ItemPedido{}); err != nil {
t.Fatalf("migrar schema de teste: %v", err)
}
t.Cleanup(func() {
sqlDB, _ := db.DB()
sqlDB.Close()
})
return db
}
func TestCriarProduto(t *testing.T) {
db := configurarDBTeste(t)
// Cria categoria primeiro
cat := Categoria{Nome: "Eletrônicos", Slug: "eletronicos"}
db.Create(&cat)
produto, err := criarProduto(db, ProdutoInput{
CategoriaID: cat.ID,
Nome: "Notebook",
Preco: 4500.00,
Estoque: 10,
})
if err != nil {
t.Fatalf("criar produto: %v", err)
}
if produto.ID == 0 {
t.Error("ID não foi gerado")
}
// Verifica no banco
buscado, err := buscarProdutoPorID(db, produto.ID)
if err != nil {
t.Fatalf("buscar produto: %v", err)
}
if buscado.Nome != "Notebook" {
t.Errorf("nome esperado 'Notebook', obtido '%s'", buscado.Nome)
}
}
func TestSoftDelete(t *testing.T) {
db := configurarDBTeste(t)
cat := Categoria{Nome: "Teste", Slug: "teste"}
db.Create(&cat)
produto, _ := criarProduto(db, ProdutoInput{
CategoriaID: cat.ID,
Nome: "Produto Soft Delete",
Preco: 100.0,
})
deletarProduto(db, produto.ID)
// Soft delete — não encontrado na busca normal
_, err := buscarProdutoPorID(db, produto.ID)
if err == nil {
t.Error("produto deletado ainda foi encontrado")
}
// Mas existe com Unscoped
var count int64
db.Unscoped().Model(&Produto{}).Where("id = ?", produto.ID).Count(&count)
if count == 0 {
t.Error("produto deveria existir com Unscoped")
}
}
Resumo do que foi coberto
Este artigo apresentou GORM em profundidade: conexão e configuração com diferentes bancos, definição de modelos com gorm.Model e tags, AutoMigrate para criação automática de schema, CRUD completo com Create, First, Updates e Delete, soft delete e delete permanente com Unscoped, consultas avançadas com filtros condicionais e scopes reutilizáveis, relacionamentos has many, belongs to e many-to-many com Preload, transações via db.Transaction, SQL bruto para casos complexos, hooks de ciclo de vida e testes com SQLite em memória. Com este artigo o Módulo 6 de banco de dados está completo.
Referências e leituras complementares
-
Documentação oficial do GORM — Guia completo com exemplos de todos os recursos. https://gorm.io/docs/
-
GORM — Associations — Referência completa de relacionamentos. https://gorm.io/docs/associations.html
-
GORM — Migrations — Estratégias de migração automática e manual. https://gorm.io/docs/migration.html
-
golang-migrate — Migrações versionadas compatíveis com GORM. https://github.com/golang-migrate/migrate
-
GORM — Hooks — Referência de todos os hooks disponíveis. https://gorm.io/docs/hooks.html
-
sqlc — Alternativa ao ORM: gera código Go tipado a partir de SQL. https://sqlc.dev