Database Migrations Seguras em Produção com Prisma
Uma coluna NOT NULL adicionada sem valor default em uma tabela com 12 milhões de linhas trava o banco por minutos. O Prisma gera a migration correta do ponto de vista de schema, mas não tem como saber que aquela ALTER TABLE vai adquirir um ACCESS EXCLUSIVE lock no Postgres e bloquear todas as queries enquanto reescreve a tabela inteira.
Esse é o tipo de problema que este post resolve: a distância entre o que o Prisma gera automaticamente e o que produção exige de verdade.
O modelo mental: migrations são deploys de infraestrutura
Tratar migration como "arquivo SQL que roda antes do app subir" é o primeiro erro. Migrations alteram a estrutura de um sistema stateful. Diferente de código aplicação (que você faz rollback com um revert de commit), uma migration que dropa uma coluna destrói dados de forma irreversível.
A regra operacional é: toda migration em produção deve ser compatível com a versão anterior E a versão seguinte do código da aplicação. Isso tem nome: expand-and-contract.
| Fase | O que acontece no banco | O que acontece no código |
|---|---|---|
| Expand | Adiciona coluna/tabela nova, sem remover nada | Código novo escreve nos dois lugares (antigo e novo) |
| Migrate | Backfill de dados, criação de índices | Código novo lê do lugar novo, escreve nos dois |
| Contract | Remove coluna/tabela antiga | Código antigo já não existe em produção |
Cada fase é um deploy separado. Tentar fazer as três em uma única migration é o caminho para downtime.
Configuração base do Prisma para ambientes reais
Antes de falar de migrations, o schema.prisma precisa estar configurado para separar ambientes corretamente:
// schema.prisma
// O shadowDatabaseUrl é obrigatório para prisma migrate dev em ambientes
// onde o usuário do banco não tem permissão CREATE DATABASE (quase todo ambiente gerenciado)
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}
A variável SHADOW_DATABASE_URL aponta para um banco descartável que o Prisma usa para calcular diffs. Em produção, o comando prisma migrate deploy nunca usa shadow database: ele aplica as migrations pendentes sequencialmente. A confusão entre migrate dev (desenvolvimento) e migrate deploy (produção) causa metade dos problemas que vejo em projetos.
# Desenvolvimento: gera migration a partir do diff entre schema.prisma e shadow database
npx prisma migrate dev --name add_status_to_orders
# Produção: aplica migrations pendentes na ordem, sem gerar nada novo
npx prisma migrate deploy
Expand-and-contract na prática com Prisma
Cenário concreto: renomear a coluna userName para displayName na tabela User.
Fase 1: Expand (adicionar coluna nova)
// schema.prisma - fase expand
model User {
id String @id @default(cuid())
email String @unique
userName String // mantém a coluna antiga
displayName String? // nova coluna, nullable para não quebrar inserts existentes
createdAt DateTime @default(now())
}
npx prisma migrate dev --name add_display_name_to_user
O código da aplicação passa a escrever nas duas colunas:
// user-service.ts - fase expand
// Escrita dual garante que tanto código antigo quanto novo encontram dados válidos
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function updateUserName(userId: string, newName: string) {
return prisma.user.update({
where: { id: userId },
data: {
userName: newName,
displayName: newName, // escrita dual: popula a coluna nova em paralelo
},
});
}
Fase 2: Migrate (backfill)
Crie uma migration SQL customizada para preencher os registro
Leia o artigo completo em https://www.vivodecodigo.com.br/backend/database-migrations-seguras-producao-prisma