A migration rodou liso. Aí você deu deploy.
Você criou a migration, rodou php artisan migrate no seu PC, tudo verdinho. Testou, funcionou, subiu pra produção numa sexta às 18h porque "é só uma coluninha".
E aí o deploy travou. Ou pior: rodou, mas a tabela de 8 milhões de linhas ficou travada por 40 segundos e o site deu timeout pra todo mundo.
Migration é traiçoeira assim. No banco vazio da sua máquina tudo é rápido e reversível. Em produção, com dados de verdade e usuários online, cada comando tem consequência. Bora ver os cuidados que separam um deploy tranquilo de um plantão não planejado.
Regra 1: o down tem que existir e funcionar
Toda migration tem up e down. O up você caprichou. O down? Geralmente tá vazio ou errado — e você só descobre no pior momento, quando precisa de um rollback às pressas.
public function up(): void
{
Schema::table('pedidos', function (Blueprint $table) {
$table->string('rastreio')->nullable();
});
}
public function down(): void
{
Schema::table('pedidos', function (Blueprint $table) {
$table->dropColumn('rastreio'); // o inverso EXATO do up
});
}
Regra de bolso: se você não consegue escrever um down que desfaz o up, sua migration provavelmente tá fazendo coisa demais. Teste o rollback antes do deploy: migrate e depois migrate:rollback na sua máquina. Se voltar limpo, respira aliviado.
Regra 2: nunca edite uma migration que já rodou em produção
Essa dói porque é tentador. Você criou a coluna com o tamanho errado, abre a migration antiga e conserta ali mesmo. No seu PC funciona (você deu migrate:fresh). Em produção, aquela migration já rodou — o Laravel não vai executá-la de novo. A correção nunca chega no servidor.
O certo é sempre uma migration nova:
// Nova migration, não mexe na antiga
Schema::table('users', function (Blueprint $table) {
$table->string('name', 100)->change();
});
Migration é histórico. Você não reescreve o passado, você adiciona um capítulo.
Regra 3: adicionar coluna? nullable ou com default
Adicionar uma coluna NOT NULL sem valor padrão numa tabela que já tem dados é pedido de erro na certa — o banco não sabe o que colocar nas linhas existentes.
// 💣 explode se a tabela já tiver linhas
$table->string('cpf');
// ✅ seguro
$table->string('cpf')->nullable();
// ou
$table->boolean('ativo')->default(true);
Precisa mesmo que a coluna seja obrigatória e preenchida? Faz em três passos: cria nullable, preenche os dados, depois torna obrigatória. Nunca tudo de uma vez numa tabela grande.
Regra 4: dropColumn apaga dados. Pra sempre.
Rollback de coluna adicionada é tranquilo. Rollback de coluna removida não existe — o down recria a coluna vazia, mas os dados que estavam lá já foram pro além.
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('bio'); // os dados somem AGORA
});
}
Antes de dropar em produção: tem backup? Tem certeza que nada mais usa essa coluna? Uma prática segura é deixar a coluna morrer aos poucos — para de usá-la no código num deploy, e só remove no banco semanas depois, quando tem certeza absoluta.
Regra 5: separe mudança de schema de mudança de dados
A tentação é preencher os dados na mesma migration que altera a estrutura. O problema: um User::all() dentro da migration pode carregar milhões de registros na memória e travar o deploy.
Se precisar transformar dados, use chunk pra não estourar a memória — ou, melhor ainda, jogue isso num comando/job separado que roda depois da migration:
// Se for na migration mesmo, ao menos vá de chunk:
User::where('tipo', null)->chunkById(500, function ($users) {
foreach ($users as $user) {
$user->update(['tipo' => 'comum']);
}
});
Schema é uma coisa, dado é outra. Misturar os dois numa migration só é onde os deploys vão morrer.
Regra 6: no deploy, sempre --force
Em produção o Laravel pergunta "tem certeza?" antes de rodar migration — e num pipeline automatizado isso trava esperando um "yes" que nunca vem. A flag --force pula a confirmação:
php artisan migrate --force
Coloca isso no seu script de deploy e nunca mais veja o pipeline pendurado esperando input.
Pegadinha: tabela grande = lock demorado
Num MySQL, alterar uma coluna (change()) ou adicionar índice numa tabela gigante pode travar a tabela inteira durante a operação. No seu PC com 200 linhas é instantâneo. Em produção com milhões, são segundos (ou minutos) de tabela indisponível.
Pra tabelas realmente grandes, vale olhar ferramentas de migração online (como pt-online-schema-change) ou rodar a alteração numa janela de baixo tráfego. Só de saber que o lock existe você já evita a surpresa.
Bônus: seu checklist de pré-deploy
Antes de dar push numa migration, passa por essa lista mental:
- O
downdesfaz oup? Testei o rollback local? - Estou editando uma migration antiga? (Se sim, para. Cria uma nova.)
- Coluna nova é
nullableou temdefault? - Tem
dropColumn? Tem backup e certeza? - Tem manipulação de dados pesada? Dá pra separar num job?
Antes de você fechar a aba
Migration não é código descartável — é a coisa que roda direto no seu banco de produção, sem rede de proteção. Um pouco de paranoia aqui economiza muitos plantões.
Me conta aí embaixo: qual migration já te deu o maior susto em produção? Todo dev tem uma história dessas. 😅 E se esse checklist te salvou de um deploy na sexta, salva o post e manda pro colega que ainda edita migration antiga.
Top comments (0)