DEV Community

Denis Augusto
Denis Augusto

Posted on

Migrations que não quebram em produção — o guia que ninguém te deu

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
    });
}
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
    });
}
Enter fullscreen mode Exit fullscreen mode

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']);
    }
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 down desfaz o up? Testei o rollback local?
  • Estou editando uma migration antiga? (Se sim, para. Cria uma nova.)
  • Coluna nova é nullable ou tem default?
  • 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)