Introdução ao GORM e por que ele é indispensável
GORM é a biblioteca ORM (Object-Relational Mapping) mais popular e robusta do ecossistema Go. Ela abstrai a complexidade de interagir diretamente com bancos de dados SQL, permitindo que você trabalhe com dados através de structs Go em vez de escrever queries SQL manualmente. Isso não significa que você abandona SQL — muito pelo contrário. GORM é uma ferramenta que simplifica operações comuns enquanto oferece escape hatches para casos complexos.
A razão pela qual GORM é tão valorizada em projetos profissionais é que ela reduz boilerplate, melhora a segurança contra SQL injection através de prepared statements, e mantém seu código Go idiomático. Se você trabalha com qualquer banco de dados relacional em Go (PostgreSQL, MySQL, SQLite), GORM será inevitavelmente parte de seu toolbox.
Configuração Inicial e Conexão com Banco de Dados
Instalação e Setup Básico
Comece instalando GORM e o driver do banco de dados desejado. Aqui usaremos PostgreSQL como exemplo, mas o conceito aplica-se a qualquer banco suportado.
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
A conexão com o banco de dados é o primeiro passo. Você estabelece uma conexão e a mantém aberta para toda a aplicação:
package main
import (
"fmt"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func main() {
dsn := "host=localhost user=postgres password=password dbname=myapp port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("Failed to connect to database")
}
fmt.Println("Connected successfully")
}
O gorm.DB retornado é a instância que você usará em toda sua aplicação. É recomendado injetar essa instância via dependency injection ou armazená-la em um contexto. Nunca crie novas conexões em cada requisição — reutilize a mesma instância.
Definindo Modelos
Modelos em GORM são simples structs Go com tags que mapeiam para colunas do banco de dados. GORM usa convenções (como pluralizar nomes de tabelas automaticamente) mas permite customização completa:
package models
import "time"
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"column:name;size:100;not null"`
Email string `gorm:"column:email;size:255;uniqueIndex:,type:btree"`
Age int `gorm:"column:age"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// TableName sobrescreve o nome da tabela (plural automático seria "users")
func (User) TableName() string {
return "users"
}
As tags definem o comportamento no banco de dados: primaryKey marca a chave primária, not null enforça NOT NULL, uniqueIndex cria um índice único. DeletedAt implementa soft delete — registros não são deletados fisicamente, apenas marcados como deletados. GORM automaticamente os exclui de queries normais.
Migrations: Versionando seu Esquema de Banco de Dados
O que são Migrations e por que importam
Migrations são código versionado que define e altera o esquema do banco de dados. Em vez de executar comandos SQL manualmente (o que é propenso a erros e não é rastreável), você escreve migrations que podem ser executadas, revertidas e auditadas. GORM oferece suporte nativo a migrations através de AutoMigrate para casos simples e migrations customizadas para casos complexos.
AutoMigrate: O Caminho Rápido
AutoMigrate é perfeito para desenvolvimento inicial e prototipagem. Ele examina seus modelos e cria tabelas, colunas e índices automaticamente. Se você adicionar novos campos, ele adiciona as colunas (sem perder dados existentes):
package main
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"myapp/models"
)
func main() {
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
// Cria tabelas para todos os modelos
db.AutoMigrate(
&models.User{},
&models.Post{},
&models.Comment{},
)
}
O AutoMigrate é idempotente — você pode rodá-lo várias vezes sem causar erro. Se a tabela já existe, ele apenas verifica se há colunas faltando e as adiciona.
Migrations Customizadas para Controle Total
Em ambientes de produção, é comum usar migrations explícitas para ter controle granular sobre o que muda. GORM integra-se com bibliotecas de migration como golang-migrate, mas você também pode escrever migrations customizadas:
package migrations
import (
"gorm.io/gorm"
"myapp/models"
)
func CreateUserTable(db *gorm.DB) error {
// Migração encapsulada em função
return db.Migrator().CreateTable(&models.User{})
}
func AddEmailUniqueConstraint(db *gorm.DB) error {
return db.Migrator().CreateIndex(&models.User{}, "email")
}
func DropUserTable(db *gorm.DB) error {
return db.Migrator().DropTable(&models.User{})
}
Em um projeto real, você organizaria migrations em arquivos com timestamp (ex: 001_create_users.go, 002_add_posts.go) e teria um sistema que as executa em ordem. Isso garante rastreabilidade e permite reverter mudanças se necessário.
CRUD Operations: Criando, Lendo, Atualizando e Deletando Dados
Create: Inserindo Dados
Criar registros em GORM é direto — você instancia a struct, popula com dados e chama Create:
user := models.User{
Name: "Alice",
Email: "alice@example.com",
Age: 28,
}
result := db.Create(&user)
if result.Error != nil {
panic(result.Error)
}
fmt.Println(user.ID) // ID auto-gerado pelo banco está aqui
Create retorna um *gorm.DB com informações sobre a operação. result.Error contém qualquer erro ocorrido. result.RowsAffected diz quantas linhas foram inseridas. O campo ID é automaticamente populado após inserção se a coluna for autoincrement.
Para inserir múltiplos registros, use CreateInBatches para melhor performance:
users := []models.User{
{Name: "Bob", Email: "bob@example.com"},
{Name: "Charlie", Email: "charlie@example.com"},
{Name: "Diana", Email: "diana@example.com"},
}
db.CreateInBatches(users, 100) // Insere em lotes de 100
Read: Recuperando Dados
Leitura é onde GORM brilha. Ele oferece uma API fluent para consultas:
var user models.User
// Buscar por chave primária
db.First(&user, 1) // SELECT * FROM users WHERE id = 1
// Buscar com condição
db.Where("email = ?", "alice@example.com").First(&user)
// Buscar múltiplos registros
var users []models.User
db.Where("age > ?", 25).Find(&users)
// Com ordenação e limite
db.Order("created_at DESC").Limit(10).Find(&users)
// Selecionar colunas específicas
db.Select("id", "name").Find(&users)
O padrão é sempre: db.Where(...).Order(...).Limit(...).Find(&variable). A variável destino recebe os resultados — use um valor único para First ou uma slice para Find.
Caso nenhum resultado seja encontrado, result.Error conterá gorm.ErrRecordNotFound. Sempre verifique:
result := db.First(&user, 1)
if result.Error == gorm.ErrRecordNotFound {
fmt.Println("Usuário não encontrado")
} else if result.Error != nil {
panic(result.Error)
}
Update: Alterando Dados
Atualizar dados requer cuidado para não fazer mudanças não intencionais. GORM oferece várias formas:
// Atualizar um registro específico
db.Model(&models.User{}).Where("id = ?", 1).Update("name", "Alice Updated")
// Atualizar múltiplos campos
db.Model(&models.User{}).Where("id = ?", 1).Updates(models.User{
Name: "Alice",
Age: 29,
})
// Atualizar usando um map (para campos dinâmicos)
db.Model(&models.User{}).Where("id = ?", 1).Updates(map[string]interface{}{
"name": "Alice",
"age": 30,
})
Note que Update (singular) usa um valor e chave, enquanto Updates (plural) usa uma struct ou map. Model especifica qual tipo de dados você está atualizando e é geralmente necessário.
Delete: Removendo Dados
Deletar também requer cautela. Se seu modelo tem DeletedAt, GORM faz soft delete por padrão:
// Soft delete (marca como deletado, não remove fisicamente)
db.Delete(&models.User{}, 1)
// Hard delete (remove fisicamente)
db.Unscoped().Delete(&models.User{}, 1)
// Queries automaticamente excluem soft-deleted
var users []models.User
db.Find(&users) // Não inclui deletados
// Para incluir deletados
db.Unscoped().Find(&users)
Relacionamentos: Conectando Dados entre Tabelas
One-to-Many: Um para Muitos
O relacionamento mais comum é one-to-many. Um usuário tem muitos posts. Defina assim:
type User struct {
ID uint
Name string
Posts []Post // Slice de posts
}
type Post struct {
ID uint
Title string
UserID uint // Foreign key
User User // Relacionamento inverso
}
Quando você chama AutoMigrate com esses modelos, GORM automaticamente adiciona a coluna UserID em Post e cria a constraint de chave estrangeira. UserID mapeia para User.ID por convenção.
Ao carregar dados, use Preload para popular relacionamentos:
var user models.User
// Carrega user E seus posts em uma query
db.Preload("Posts").First(&user, 1)
// Agora user.Posts está populado
for _, post := range user.Posts {
fmt.Println(post.Title)
}
// Pode fazer preload condicional
db.Preload("Posts", "published = ?", true).First(&user, 1)
Preload carrega dados em queries separadas (mais eficiente para aplicações Go). Joins faria um SQL JOIN se você preferir.
Many-to-Many: Muitos para Muitos
Quando duas entidades têm relação muitos-para-muitos (ex: usuários e papéis), você precisa de uma tabela de junção:
type User struct {
ID uint
Name string
Roles []Role `gorm:"many2many:user_roles"`
}
type Role struct {
ID uint
Name string
}
GORM cria a tabela user_roles automaticamente com colunas user_id e role_id. Para associar dados:
var user models.User
db.First(&user, 1)
var role models.Role
db.First(&role, 1)
// Associar role a user
db.Model(&user).Association("Roles").Append(&role)
// Carregar roles
db.Preload("Roles").First(&user, 1)
// Desassociar
db.Model(&user).Association("Roles").Delete(&role)
Belongs-to: Pertence a
BelongsTo define o inverso de um relacionamento. Um post pertence a um usuário:
type Post struct {
ID uint
Title string
UserID uint // Foreign key
User User // Carregado via Preload
}
type User struct {
ID uint
Name string
}
Quando você faz db.Preload("User").First(&post, 1), GORM carrega o usuário associado:
var post models.Post
db.Preload("User").First(&post, 1)
fmt.Println(post.User.Name) // Nome do usuário
Exemplo Prático Completo: Blog com Posts e Comentários
Vamos criar um exemplo real que combina conceitos:
package models
import "time"
type User struct {
ID uint
Name string
Email string `gorm:"uniqueIndex"`
Posts []Post
CreatedAt time.Time
UpdatedAt time.Time
}
type Post struct {
ID uint
Title string
Content string
UserID uint // Foreign key
User User // Belongs to
Comments []Comment // One-to-many
CreatedAt time.Time
}
type Comment struct {
ID uint
Text string
PostID uint
Post Post
UserID uint
User User
CreatedAt time.Time
}
Operações:
// Criar usuário com posts
user := models.User{Name: "John", Email: "john@example.com"}
db.Create(&user)
post := models.Post{Title: "Hello World", Content: "...", UserID: user.ID}
db.Create(&post)
// Carregar tudo com relacionamentos
var fullUser models.User
db.Preload("Posts", func(db *gorm.DB) *gorm.DB {
return db.Preload("Comments").Order("created_at DESC")
}).First(&fullUser, user.ID)
// Agora temos user com todos posts, e cada post com seus comentários
for _, post := range fullUser.Posts {
fmt.Printf("Post: %s\n", post.Title)
for _, comment := range post.Comments {
fmt.Printf(" Comment: %s\n", comment.Text)
}
}
Hooks e Callbacks: Automatizando Comportamentos
GORM oferece hooks que permitem executar código em momentos específicos do ciclo de vida de um modelo. Use isso para validações, auditorias e transformações de dados:
func (u *User) BeforeSave(tx *gorm.DB) error {
// Validação antes de salvar
if len(u.Name) == 0 {
return fmt.Errorf("name cannot be empty")
}
return nil
}
func (u *User) AfterCreate(tx *gorm.DB) error {
// Executado após criar
fmt.Printf("Usuário %s criado com ID %d\n", u.Name, u.ID)
return nil
}
func (p *Post) BeforeDelete(tx *gorm.DB) error {
// Limpar antes de deletar
fmt.Printf("Deletando post %s\n", p.Title)
return nil
}
Hooks são poderosos mas use com moderação — lógica complexa em hooks torna o código difícil de debugar. Mantenha hooks simples e coloque lógica complexa em services/repositories.
Tratamento de Erros e Debugging
Toda operação GORM retorna *gorm.DB com um campo Error. Sempre verifique:
result := db.Create(&user)
if result.Error != nil {
// Pode ser violação de constraint, erro de conexão, etc
log.Printf("Erro ao criar usuário: %v", result.Error)
// Trate apropriadamente
}
Para debugging, ative o logger do GORM:
import "gorm.io/logger"
db, _ := gorm.Open(
postgres.Open(dsn),
&gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
},
)
Isso imprime todas as queries executadas, útil para otimizar e entender o que está acontecendo. Em produção, use um logger mais sofisticado com níveis configuráveis.
Boas Práticas e Performance
N+1 Queries: O Vilão Comum
Um erro clássico é carregar dados sem preload:
// RUIM: N+1 queries
var users []models.User
db.Find(&users)
for _, user := range users {
db.Find(&user.Posts) // Query adicional para cada usuário!
}
// BOM: Uma query com preload
var users []models.User
db.Preload("Posts").Find(&users)
Sempre use Preload quando souber que precisará de relacionamentos.
Índices e Constraints
Defina índices nas colunas que filtra frequentemente:
type Post struct {
ID uint
Title string
UserID uint `gorm:"index"` // Índice simples
Status string `gorm:"index:idx_status_user,type:btree"` // Índice composto
CreatedAt time.Time `gorm:"index:,type:btree,sort:desc"`
}
Índices melhoram performance de leitura mas custam espaço e desaceleram escritas. Use sabiamente.
Connection Pooling
GORM reutiliza a mesma conexão, mas você pode configurar o pool:
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
Essas configurações dependem da sua aplicação e banco de dados. Monitore métricas de conexão em produção.
Conclusão
Você agora domina os conceitos fundamentais de GORM: (1) Migrations são seu instrumento para versionamento e auditoria de esquemas — use AutoMigrate para desenvolvimento e migrations customizadas para produção com precisão. (2) Relacionamentos (one-to-many, many-to-many, belongs-to) permitem modelar dados complexos de forma idiomática em Go — sempre use Preload para evitar N+1 queries. (3) CRUD operations em GORM são simples e seguras contra SQL injection, mas exigem compreensão de quando usar Create, First, Find, Update e Delete e sempre verificar result.Error.