Migrações Laravel: adicionar uma coluna, reverter, apagar

Introdução Acrescentar colunas ou tabelas à mão na base de dados é um processo intimidante e leva quase sempre a incoerências entre os teus vários ambientes. As migrações do Laravel permitem-te versionar a base de dados para que todos os membros da tua equipa possam dispor de um

Migrações Laravel: adicionar uma coluna, reverter, apagar
Resposta rápida

Uma migração apaga-se eliminando o seu ficheiro, mas só se nunca tiver corrido. Verifica com php artisan migrate:status: se estiver em Ran, lança primeiro php artisan migrate:rollback --step=1 e só depois apaga o ficheiro. Para acrescentar uma coluna a uma tabela existente, nunca alteres a migração antiga: cria uma nova com --table.

As migrações versionam o esquema da tua base de dados como o Git versiona o teu código: cada alteração é um ficheiro, que se pode voltar a correr em qualquer ambiente. Três operações cobrem o essencial do dia a dia: acrescentar uma coluna a uma tabela que já existe, reverter uma migração e apagar um ficheiro que nunca devia ter sido criado.

Artigo do percurso Desenvolvimento web. Todos os comandos e todas as saídas abaixo foram executados em Laravel 13.30.1 com PHP 8.4.25.

Criar uma migração

O comando segue uma convenção de nomes que determina o conteúdo do ficheiro gerado:

bash
php artisan make:migration create_tasks_table

O nome começa por create, seguido do nome da tabela no plural, seguido de table. O Laravel reconhece este padrão e pré-preenche Schema::create() com id() e timestamps(), a coluna title é acrescentada aqui para o exemplo, e os comentários gerados foram retirados:

database/migrations/2026_09_02_100000_create_tasks_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};
Se as tuas migrações não se parecem com isto

Desde o Laravel 8, as migrações são classes anónimas devolvidas por return new class extends Migration. As migrações antigas com nome, do tipo class CreateTasksTable extends Migration, continuam a funcionar mas já não são geradas. Do mesmo modo, $table->bigIncrements('id') deu lugar a $table->id(), e os métodos up() e down() declaram agora um tipo de retorno void.

Se a tabela precisar de datas de criação e de alteração, $table->timestamps() acrescenta-as: é a ausência delas que provoca o erro «Unknown column ‘updated_at’».

Executar a migração cria a tabela:

bash
php artisan migrate

Acrescentar uma coluna a uma tabela existente

Com o esquema no sítio, as consultas Eloquent do dia a dia fazem o resto. Nunca alteres uma migração que já correu: os teus colegas e os teus servidores já a executaram e não a voltarão a executar. Cria uma migração nova, indicando a tabela em causa com --table:

bash
php artisan make:migration add_notes_to_tasks_table --table=tasks

O ficheiro gerado contém um Schema::table() vazio nos dois sentidos. Preenche-os: a coluna no up(), a sua remoção no down().

database/migrations/2026_09_02_194036_add_notes_to_tasks_table.php
public function up(): void
{
    Schema::table('tasks', function (Blueprint $table) {
        $table->text('notes')->nullable()->after('title');
    });
}

public function down(): void
{
    Schema::table('tasks', function (Blueprint $table) {
        $table->dropColumn('notes');
    });
}
bash
php artisan migrate
code
  2026_09_02_194036_add_notes_to_tasks_table .................... 47.43ms DONE

Há duas precauções que vale a pena recordar aqui.

Torna a coluna nullable, ou dá-lhe um valor por omissão. Numa tabela que já contém linhas, uma coluna NOT NULL sem valor por omissão faz falhar a migração: a base de dados não sabe o que escrever nas linhas existentes.

Escreve sempre o down(). É ele que torna a migração reversível. Uma migração sem down() transforma o mais pequeno passo atrás numa intervenção manual.

after() não funciona em todo o lado

Posicionar uma coluna com after() é próprio do MySQL e do MariaDB. No SQLite, a instrução é aceite sem erro mas ignorada: a coluna é acrescentada no fim da tabela. Verificado em SQLite 3.46.1, onde notes acaba depois de updated_at apesar do after('title'). A ordem das colunas não tem qualquer efeito funcional.

Reverter uma migração já executada

Começa por ver onde estás. O migrate:status lista cada migração com o seu lote e o seu estado:

bash
php artisan migrate:status
code
  2026_09_02_100000_create_tasks_table ............................... [1] Ran
  2026_09_02_194036_add_notes_to_tasks_table ......................... Pending

Uma migração Pending nunca foi executada: o ficheiro pode ser apagado diretamente. Uma migração Ran tem de ser revertida primeiro.

bash
# reverte apenas a última migração
php artisan migrate:rollback --step=1

# reverte todo o último lote
php artisan migrate:rollback

O rollback executa o método down(). No exemplo anterior, a coluna notes desaparece mesmo da tabela e a migração volta a Pending. É nessa altura, e não antes, que podes apagar o ficheiro.

Existem dois comandos vizinhos, a reservar para o desenvolvimento:

bash
# reverte tudo e volta a correr tudo
php artisan migrate:refresh

# elimina todas as tabelas e volta a correr tudo
php artisan migrate:fresh
migrate:fresh apaga os dados

O migrate:fresh elimina as tabelas, incluindo as que nenhuma migração gere. Num servidor de produção, o comando destrói a base de dados sem confirmação possível depois de lançado. Reserva-o estritamente para o desenvolvimento local.

Apagar uma migração

Não existe nenhum make:migration --delete: uma migração apaga-se eliminando o seu ficheiro, em database/migrations, a pasta que database_path() devolve entre os caminhos da aplicação. A única questão é saber se já correu.

Nunca foi executada (estado Pending): apaga o ficheiro, mais nada.

bash
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.php

Já foi executada (estado Ran): reverte-a primeiro e só depois apaga o ficheiro.

bash
php artisan migrate:rollback --step=1
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.php
Se a migração já seguiu para outro ambiente

Apagar o ficheiro não elimina a linha correspondente na tabela migrations das bases onde ela já correu. Esses ambientes ficam com a coluna, sem nenhuma migração que a explique. Nesse caso, não apagues nada: escreve uma nova migração que desfaça a alteração. O princípio é o mesmo do Git, onde se prefere um commit de anulação a reescrever um histórico já publicado.

Alterar uma coluna existente

Para mudar um tipo, um comprimento ou a nulabilidade, volta a declarar a coluna e acrescenta change():

php
public function up(): void
{
    Schema::table('tasks', function (Blueprint $table) {
        $table->string('title', 500)->change();
    });
}

Boa notícia para quem vem de uma versão antiga: o pacote doctrine/dbal, durante muito tempo obrigatório para esta operação, deixou de o ser a partir do Laravel 11. Verificado em Laravel 13.30.1, onde change() funciona sem dependência adicional.

change() reescreve toda a definição

Os atributos que não repetires perdem-se. Uma coluna ->nullable()->default('x') alterada por um simples ->string('title', 500)->change() volta a ser não nula e sem valor por omissão. Volta a declarar a definição completa de cada vez.

Erros frequentes

Alterar uma migração já executada Os outros ambientes não a voltam a correr e ficam com o esquema antigo. Cria sempre uma migração nova.
Coluna NOT NULL sem valor por omissão Numa tabela que já contém linhas, a migração falha: a base de dados não sabe o que escrever no que já existe. Acrescenta nullable() ou default().
Esquecer o método down() A migração torna-se irreversível e o mais pequeno passo atrás passa a ser uma intervenção manual.
migrate:fresh em produção O comando elimina todas as tabelas, incluindo as que nenhuma migração gere, sem pedir confirmação.
after() ignorado fora do MySQL No SQLite, a coluna é acrescentada no fim da tabela, sem erro. Sem efeito funcional, mas o esquema deixa de corresponder ao que está escrito.
change() reescreve toda a definição Os atributos não repetidos perdem-se: uma coluna nullable com valor por omissão perde ambos se não os voltares a declarar.

LaravelMigrationsPHPSQL

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.