
Eine Migration löschst du, indem du ihre Datei entfernst, aber nur, wenn sie nie gelaufen ist. Prüf das mit php artisan migrate:status: Steht sie auf Ran, führ zuerst php artisan migrate:rollback --step=1 aus und lösch dann die Datei. Um eine Spalte zu einer bestehenden Tabelle hinzuzufügen, änder nie die alte Migration: Leg eine neue mit --table an.
Migrationen versionieren dein Datenbankschema so, wie Git deinen Code versioniert: Jede Änderung ist eine Datei und lässt sich auf jeder Umgebung erneut abspielen. Drei Operationen decken den Alltag weitgehend ab: eine Spalte zu einer bestehenden Tabelle hinzufügen, eine Migration zurückrollen und eine Datei löschen, die nie hätte entstehen dürfen.
Artikel aus dem Themenpfad Webentwicklung. Alle Befehle und Ausgaben unten liefen auf Laravel 13.30.1 mit PHP 8.4.25.
Eine Migration anlegen
Der Befehl folgt einer Namenskonvention, die über den Inhalt der erzeugten Datei entscheidet:
php artisan make:migration create_tasks_tableDer Name beginnt mit create, gefolgt vom Tabellennamen im Plural und von table. Laravel erkennt dieses Muster und füllt Schema::create() mit id() und timestamps() vor, die Spalte title ist hier für das Beispiel hinzugefügt, und die generierten Kommentare wurden entfernt:
<?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');
}
};Seit Laravel 8 sind Migrationen anonyme Klassen, zurückgegeben über return new class extends Migration. Ältere benannte Migrationen der Form class CreateTasksTable extends Migration funktionieren weiterhin, werden aber nicht mehr erzeugt. Ebenso hat $table->bigIncrements('id') dem $table->id() Platz gemacht, und die Methoden up() und down() deklarieren inzwischen den Rückgabetyp void.
Soll die Tabelle ein Erstellungs- und ein Änderungsdatum führen, fügt $table->timestamps() beide hinzu: Fehlen sie, bekommst du den Fehler „Unknown column ‚updated_at’“.
Die Migration auszuführen legt die Tabelle an:
php artisan migrateEine Spalte zu einer bestehenden Tabelle hinzufügen
Steht das Schema, erledigen die gängigen Eloquent-Abfragen den Rest. Ändere nie eine Migration, die bereits gelaufen ist: Deine Kollegen und deine Server haben sie abgespielt und spielen sie kein zweites Mal. Leg eine neue Migration an und gib die betroffene Tabelle mit --table an:
php artisan make:migration add_notes_to_tasks_table --table=tasksDie erzeugte Datei enthält ein leeres Schema::table() in beide Richtungen. Füll beide: die Spalte in up(), ihr Entfernen in down().
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');
});
}php artisan migrate 2026_09_02_194036_add_notes_to_tasks_table .................... 47.43ms DONEZwei Vorsichtsmaßnahmen sind hier eine Erinnerung wert.
Mach die Spalte nullable oder gib ihr einen Standardwert. Auf einer Tabelle, die schon Zeilen enthält, lässt eine NOT NULL-Spalte ohne Standardwert die Migration scheitern: Die Datenbank weiß nicht, was sie in die bestehenden Zeilen schreiben soll.
Schreib immer das down(). Erst dadurch wird die Migration umkehrbar. Eine Migration ohne down() macht aus dem kleinsten Rückzieher Handarbeit.
Eine Spalte mit after() zu platzieren ist MySQL und MariaDB eigen. Unter SQLite wird die Anweisung fehlerfrei angenommen, aber ignoriert: Die Spalte landet am Ende der Tabelle. Geprüft auf SQLite 3.46.1, wo notes trotz after('title') hinter updated_at steht. Die Reihenfolge der Spalten hat keinerlei funktionale Auswirkung.
Eine bereits ausgeführte Migration zurückrollen
Sieh zuerst nach, wo du stehst. migrate:status listet jede Migration mit ihrem Batch und ihrem Zustand:
php artisan migrate:status 2026_09_02_100000_create_tasks_table ............................... [1] Ran
2026_09_02_194036_add_notes_to_tasks_table ......................... PendingEine Migration im Zustand Pending ist nie gelaufen: Ihre Datei kannst du direkt löschen. Eine Migration im Zustand Ran muss zuerst zurückgerollt werden.
# rollt nur die letzte Migration zurück
php artisan migrate:rollback --step=1
# rollt den gesamten letzten Batch zurück
php artisan migrate:rollbackDas Rollback führt die Methode down() aus. Im Beispiel oben verschwindet die Spalte notes tatsächlich aus der Tabelle, und die Migration steht wieder auf Pending. Genau dann, und keinen Moment früher, darfst du die Datei löschen.
Zwei verwandte Befehle gibt es noch, beide gehören in die Entwicklung:
# rollt alles zurück und spielt alles neu ab
php artisan migrate:refresh
# löscht alle Tabellen und spielt danach alles neu ab
php artisan migrate:freshmigrate:fresh wirft die Tabellen weg, auch die, um die sich keine Migration kümmert. Auf einem Produktionsserver zerstört der Befehl die Datenbank, einmal gestartet ohne jede Rückfrage. Halte ihn strikt der lokalen Entwicklung vor.
Eine Migration löschen
Ein make:migration --delete gibt es nicht: Eine Migration löschst du, indem du ihre Datei in database/migrations entfernst, dem Verzeichnis, das database_path() unter den Pfaden der Anwendung zurückgibt. Die einzige Frage ist, ob sie schon gelaufen ist.
Sie ist nie gelaufen (Zustand Pending): Lösch die Datei, mehr nicht.
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.phpSie ist bereits gelaufen (Zustand Ran): Roll sie zuerst zurück, dann lösch die Datei.
php artisan migrate:rollback --step=1
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.phpDie Datei zu löschen entfernt nicht die zugehörige Zeile in der Tabelle migrations der Datenbanken, auf denen sie bereits lief. Diese Umgebungen behalten die Spalte, ohne dass noch irgendeine Migration sie erklärt. Lösch in dem Fall gar nichts: Schreib eine neue Migration, die die Änderung rückgängig macht. Das Prinzip ist dasselbe wie bei Git, wo ein Revert-Commit dem Umschreiben bereits veröffentlichter Historie vorzuziehen ist.
Eine bestehende Spalte ändern
Um einen Typ, eine Länge oder die Nullbarkeit zu ändern, deklarierst du die Spalte erneut und hängst change() an:
public function up(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->string('title', 500)->change();
});
}Gute Nachricht für alle, die von einer alten Version herkommen: Das Paket doctrine/dbal, lange Pflicht für diese Operation, ist es seit Laravel 11 nicht mehr. Geprüft auf Laravel 13.30.1, wo change() ohne zusätzliche Abhängigkeit funktioniert.
Attribute, die du nicht wiederholst, sind verloren. Eine Spalte ->nullable()->default('x'), geändert durch ein schlichtes ->string('title', 500)->change(), ist danach wieder nicht nullable und ohne Standardwert. Deklarier jedes Mal die vollständige Definition.
Häufige Fehler
nullable() oder default().

