
Datenbank-Design für ein skalierbares Headless CMS

Struktur im Daten-Chaos: Kategorien ins Leben rufen
Erinnerst du dich an dein allererstes Webprojekt? Ich erinnere mich noch schmerzhaft an meines. Ein Fehler im Fundament rächt sich später immer, weshalb ein durchdachtes Laravel Datenbank Design für unser skalierbares Headless CMS absolute Pflicht ist. Aus purer Bequemlichkeit speicherte ich damals mehrere Kategorie-IDs einfach als kommagetrennten Text in einer einzigen Datenbankspalte ab. Das schien für den Moment unglaublich clever. Ein paar Monate später forderte der Kunde plötzlich eine Filterfunktion. Mein Hack brach in sich zusammen, und die SQL-Abfragen wurden zu einem langsamen, monströsen Albtraum.
Ein Fehler im Fundament rächt sich später immer. Ein durchdachtes Laravel Datenbank Design ist der einzige Weg, um solche architektonischen Katastrophen zu vermeiden. Wenn wir jetzt unser Headless CMS skalierbar aufbauen wollen, dürfen wir nicht in flachen Tabellen denken. Wir müssen ein intelligentes, relationales Netzwerk spannen.
Ein gutes CMS lebt von Verbindungen. Ein Artikel (Post) wird von einem Autor (User) geschrieben und gehört zu einer bestimmten Rubrik (Category). Wie bringen wir unserer API bei, diese Zusammenhänge blitzschnell und effizient auszuliefern? Genau das bauen wir jetzt auf.
Schritt 1: Die Kategorien ins Leben rufen
Im ersten Teil unserer Serie haben wir bereits ein isoliertes Post-Modell erschaffen. Für ein echtes Laravel Datenbank Design schweben unsere Artikel momentan aber noch zu orientierungslos im luftleeren Raum. Wir brauchen Kategorien.
Öffne dein Terminal in deinem Laravel-Projektordner (cms-backend). Wir nutzen wieder die magischen Artisan-Befehle, um uns die lästige Handarbeit zu ersparen.
Tippe folgenden Befehl ein:
php artisan make:model Category -mfDas -mf Flag kennen wir bereits: Es generiert neben dem Modell (Category.php) sofort die passende Migration und eine Factory für spätere Testdaten.
Laravel hat nun im Ordner database/migrations eine neue Datei angelegt, die auf _create_categories_table.php endet. Öffne diese Datei in deinem Code-Editor.
Hier definieren wir die Struktur unserer Rubriken. Eine Kategorie braucht einen Namen, einen SEO-freundlichen Slug und vielleicht eine kleine Beschreibung. Passe die up() Methode exakt so an:
1public function up(): void
2{
3 Schema::create('categories', function (Blueprint $table) {
4 $table->id();
5 $table->string('name');
6 $table->string('slug')->unique();
7 $table->text('description')->nullable();
8 $table->timestamps();
9 });
10}Als Nächstes müssen wir unserem Modell mitteilen, welche Felder von außen beschrieben werden dürfen. Das ist ein extrem wichtiger Sicherheitsmechanismus (Mass Assignment Protection), um bösartige Datenbank-Injektionen über unsere spätere API zu verhindern.
Öffne die Datei app/Models/Category.php und erweitere sie um das $fillable Array:
1<?php
2
3namespace App\Models;
4
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7
8class Category extends Model
9{
10 use HasFactory;
11
12 // Diese Felder geben wir für das Befüllen über API-Anfragen frei
13 protected $fillable = [
14 'name',
15 'slug',
16 'description',
17 ];
18}Die Struktur für unsere Kategorien steht. Doch bevor wir sie in die Datenbank migrieren, müssen wir im nächsten Schritt unsere bereits bestehende Artikel-Tabelle umbauen, damit sie sich überhaupt mit dieser neuen Struktur verknüpfen lässt.

Die intelligente Brücke: Many-to-Many Relationen aufbauen
Eine Kategorie ohne Artikel ist nur ein leeres Schild. Ein Artikel ohne Kategorie ist im System quasi unsichtbar. Wir müssen diese beiden Welten verknüpfen.
Bei einem simplen Blog reicht es oft, eine category_id direkt in die posts-Tabelle zu schreiben (One-to-Many). Aber wir bauen hier kein Hobby-Projekt. Was passiert, wenn dein Redakteur später entscheidet, dass der Artikel "Next.js 15 Features" sowohl in die Rubrik "Tech" als auch in "News" gehört? Bei einer One-to-Many-Beziehung steckst du jetzt in einer architektonischen Sackgasse.
Ein exzellentes Laravel Datenbank Design zeichnet sich dadurch aus, dass es zukünftige Anforderungen antizipiert. Wir setzen daher von Beginn an auf eine Many-to-Many-Beziehung (Viele-zu-Viele). Dafür benötigen wir einen Vermittler: eine sogenannte Pivot-Tabelle.
Öffne dein Terminal und weise Laravel an, diese Vermittler-Tabelle zu erstellen:
php artisan make:migration create_category_post_tableHinweis: Die Namenskonvention in Laravel ist hier gnadenlos strikt. Pivot-Tabellen müssen aus den Singular-Namen der beiden zu verknüpfenden Tabellen bestehen, und zwar in alphabetischer Reihenfolge (c kommt vor p, also category_post).
Öffne die neu erstellte Migrations-Datei im Ordner database/migrations. Hier definieren wir nun die eisernen Regeln für unsere Brücke. Passe die up() Methode genau so an:
1public function up(): void
2{
3 Schema::create('category_post', function (Blueprint $table) {
4 $table->id();
5
6 // Wir verknüpfen die Kategorie...
7 $table->foreignId('category_id')
8 ->constrained()
9 ->cascadeOnDelete(); // Löscht die Verknüpfung, wenn die Kategorie gelöscht wird
10
11 // ...mit dem Artikel
12 $table->foreignId('post_id')
13 ->constrained()
14 ->cascadeOnDelete(); // Löscht die Verknüpfung, wenn der Artikel gelöscht wird
15
16 $table->timestamps();
17 });
18}Das cascadeOnDelete() ist unsere Versicherung gegen Datenmüll (Orphaned Records). Wenn ein Artikel jemals aus dem System entfernt wird, löscht die Datenbank automatisch auch den Eintrag auf der Brücke. Keine toten Links, absolute Datenintegrität.
Sende diese neuen Baupläne direkt an MySQL:
php artisan migrateDie Datenbank weiß nun von der Verbindung. Aber unser PHP-Code ist noch blind dafür. Wir müssen unseren Modellen beibringen, wie sie über diese Brücke gehen können.
Öffne zuerst die app/Models/Post.php und füge diese Methode hinzu:
// Ein Artikel kann vielen Kategorien angehören
public function categories()
{
return $this->belongsToMany(Category::class);
}Anschließend öffnen wir die app/Models/Category.php und definieren den exakten Gegenweg:
// Eine Kategorie kann viele Artikel beinhalten
public function posts()
{
return $this->belongsToMany(Post::class);
}Das ist die pure Eleganz von Eloquent. Mit diesen wenigen Zeilen Code haben wir ein hochkomplexes, relationales Netzwerk gespannt. Später können wir im Controller einfach $post->categories aufrufen, und Laravel holt uns in Millisekunden alle verknüpften Rubriken aus der Datenbank, ohne dass wir jemals ein manuelles SQL-JOIN schreiben müssen.

Die menschliche Komponente: Autoren in das Laravel Datenbank Design integrieren
Eine Brücke zwischen Artikeln und Kategorien haben wir erfolgreich geschlagen. Doch ein Blog ohne Gesicht wirkt kalt und anonym. Wir brauchen Autoren. Jemand muss die Verantwortung für den Content tragen.
Hier stoßen wir auf ein klassisches One-to-Many-Szenario (Eins-zu-Viele): Ein einzelner Autor kann hunderte Artikel verfassen, aber ein spezifischer Artikel wird (in unserem Setup) von exakt einem Hauptautor geschrieben. Ein perfektes Laravel Datenbank Design bildet diese Hierarchie über einen simplen Fremdschlüssel (foreign key) ab.
Da Laravel bei der Installation bereits eine vollwertige users-Tabelle für uns angelegt hat, müssen wir das Rad nicht neu erfinden. Wir müssen lediglich unsere bestehende posts-Tabelle um einen Verweis auf diesen User erweitern.
Da wir uns noch tief in der lokalen Entwicklungsphase befinden, könnten wir einfach die ursprüngliche Post-Migration umschreiben und die Datenbank komplett neu aufbauen (migrate:fresh). Aber lass uns hier direkt Best Practices für den Enterprise-Alltag trainieren. In einem Live-System würdest du niemals eine bestehende Tabelle löschen. Wir erstellen eine Änderungs-Migration.
Gehe in dein Terminal und führe diesen Befehl aus:
php artisan make:migration add_user_id_to_posts_tableÖffne die neu generierte Datei im database/migrations-Ordner. Wir fügen die user_id hinzu und definieren genau, was passieren soll, wenn ein Autor das Unternehmen verlässt und sein Account gelöscht wird.
Passe die up() und down() Methoden so an:
1public function up(): void
2{
3 Schema::table('posts', function (Blueprint $table) {
4 // Wir setzen die user_id nach der id-Spalte ein
5 $table->foreignId('user_id')
6 ->after('id')
7 ->nullable() // Falls wir Gastbeiträge erlauben wollen
8 ->constrained()
9 ->nullOnDelete(); // Wenn der User gelöscht wird, bleibt der Artikel, aber user_id wird NULL
10 });
11}
12
13public function down(): void
14{
15 Schema::table('posts', function (Blueprint $table) {
16 $table->dropForeign(['user_id']);
17 $table->dropColumn('user_id');
18 });
19}Schick dieses Update an deine MySQL-Datenbank:
php artisan migrateDie Spalte ist nun vorhanden. Jetzt bringen wir unseren Modellen bei, wie sie diese neue Verbindung nutzen können.
Öffne die Datei app/Models/Post.php und füge diese Methode hinzu:
1// Ein Artikel gehört zu genau einem User (Autor)
2 public function author()
3 {
4 // Wir nennen die Methode 'author', müssen Laravel aber sagen,
5 // dass es in der 'users' Tabelle suchen soll.
6 return $this->belongsTo(User::class, 'user_id');
7 }Anschließend ergänzen wir das exakte Gegenstück in der app/Models/User.php:
// Ein User kann viele Artikel haben
public function posts()
{
return $this->hasMany(Post::class);
}Siehst du, wie elegant das ist? Wir haben die Methode im Post-Modell bewusst author() genannt, weil es sich im Code später viel natürlicher liest ($post->author->name), obwohl es im Hintergrund auf das User-Modell zugreift. Genau diese kleinen, feinen Details machen ein durchdachtes Laravel Datenbank Design so unglaublich mächtig und lesbar für dein gesamtes Team.

Das Beziehungsgeflecht mit Leben füllen: Factories und Seeder anpassen
Ein durchdachtes Laravel Datenbank Design entfaltet seine wahre Stärke erst, wenn echte Daten durch die Adern des Systems fließen. Im ersten Teil unserer Serie haben wir 50 isolierte Artikel generiert. Jetzt, wo wir Autoren und Kategorien eingeführt haben, würde unser alter Seeder allerdings nutzlosen Datenmüll produzieren. Die neuen Artikel hätten weder ein Gesicht (Autor) noch eine Heimat (Kategorie).
Wir müssen den Maschinenraum unserer Fabrik neu kalibrieren. Laravel erlaubt es uns, dieses hochkomplexe Beziehungsgeflecht vollautomatisch zu weben, ohne dass wir jemals händisch IDs in Tabellen eintragen müssen.
Schritt 4: Den DatabaseSeeder umschreiben
Öffne in deinem Code-Editor die Datei database/seeders/DatabaseSeeder.php. Wir löschen den alten, simplen Code und orchestrieren nun eine echte Daten-Symphonie.
Wir erschaffen zuerst die Autoren, definieren dann unsere Kategorien und weisen diese Bausteine anschließend den neuen Artikeln dynamisch zu. Passe die run()-Methode exakt so an:
1<?php
2
3namespace Database\Seeders;
4
5use App\Models\Category;
6use App\Models\Post;
7use App\Models\User;
8use Illuminate\Database\Seeder;
9
10class DatabaseSeeder extends Seeder
11{
12 public function run(): void
13 {
14 // 1. Erschaffe 5 Test-Autoren (Laravel nutzt dafür die von Haus aus mitgelieferte UserFactory)
15 $authors = User::factory(5)->create();
16
17 // 2. Erschaffe 4 feste Kategorien, die in unserem CMS Sinn machen
18 $categories = Category::factory()->createMany([
19 ['name' => 'Tech', 'slug' => 'tech', 'description' => 'Alles rund um Server und Code.'],
20 ['name' => 'Design', 'slug' => 'design', 'description' => 'UI/UX, Typografie und Farben.'],
21 ['name' => 'Business', 'slug' => 'business', 'description' => 'Startups, Agenturleben und Finanzen.'],
22 ['name' => 'News', 'slug' => 'news', 'description' => 'Kurze Updates und Ankündigungen.'],
23 ]);
24
25 // 3. Generiere 50 Artikel im Speicher (make) und durchlaufe jeden einzeln (each)
26 Post::factory(50)->make()->each(function ($post) use ($authors, $categories) {
27
28 // Weise dem Artikel einen zufälligen Autor aus unserer zuvor erstellten Gruppe zu
29 $post->user_id = $authors->random()->id;
30
31 // Jetzt speichern wir den Artikel fest in die Datenbank
32 $post->save();
33
34 // Zuletzt füllen wir unsere Many-to-Many Pivot-Tabelle!
35 // Wir schnappen uns 1 bis 2 zufällige Kategorien und verknüpfen sie (attach) mit dem Artikel.
36 $post->categories()->attach(
37 $categories->random(rand(1, 2))->pluck('id')->toArray()
38 );
39 });
40 }
41}Schau dir diesen Code genau an. Anstatt mit nackten IDs zu jonglieren, nutzen wir die volle Ausdruckskraft der Objektorientierung. Die Methode attach() ist hier der absolute Gamechanger. Sie kümmert sich im Hintergrund unsichtbar um unsere category_post Pivot-Tabelle und trägt dort die korrekten Verknüpfungen ein.
Lass uns die Datenbank jetzt komplett plattmachen und mit dieser neuen Struktur sauber von null aufbauen. Führe in deinem Terminal diesen mächtigen Befehl aus:
php artisan migrate:fresh --seedDas Flag :fresh wirft alle bestehenden Tabellen gnadenlos weg, führt alle Migrationen in der perfekten Reihenfolge neu aus und ruft direkt im Anschluss unseren Seeder auf.
Dein Terminal sollte dir jetzt eine saubere Kette von erfolgreichen Migrationen und dem anschließenden Seeding anzeigen. Wenn du jetzt in dein Datenbank-Tool (wie DBngin oder TablePlus) schaust, wirst du ein voll funktionsfähiges, vernetztes Ökosystem vorfinden. Die Artikel haben Autoren, und die Pivot-Tabelle ist prall gefüllt mit Querverweisen.
Unsere Datenbank-Ebene ist nun ein architektonisches Meisterwerk. Aber unsere API liefert diese neuen, wertvollen Relationen noch gar nicht an Next.js aus.

Die API beschleunigen: Relationen laden und das N+1 Problem umgehen
Wir haben die Daten. Wir haben das perfekte Beziehungsgeflecht in der Datenbank. Doch wenn wir diese Daten jetzt naiv an unser Next.js Frontend ausliefern, tappen wir in eine der gefährlichsten und unsichtbarsten Fallen der Webentwicklung: das berüchtigte N+1 Query Problem.
Stell dir vor, unser Frontend fragt die 10 neuesten Artikel ab. Laravel macht dafür eine Datenbankabfrage (1). Jetzt durchläuft das System diese 10 Artikel und fragt für jeden einzelnen den Namen des Autors und die zugehörigen Kategorien ab (N). Aus einer einzigen, schlanken Abfrage werden plötzlich 21 Abfragen. Bei 50 Artikeln auf einer Seite wären es über 100 Abfragen. Dieser Flaschenhals bringt früher oder später jeden Server zum Weinen.
Ein exzellentes Laravel Datenbank Design beweist sich erst in der Performance, mit der es abgefragt wird. Wir weisen Laravel jetzt an, alle benötigten Relationen vorab in nur zwei oder drei massiven, hochoptimierten Abfragen zu laden. Dieses Konzept nennt sich "Eager Loading".
Schritt 5: Eager Loading im Controller aktivieren
Öffne deinen PostController unter app/Http/Controllers/Api/PostController.php. Wir modifizieren unsere Datenbankabfrage und nutzen die with() Methode, um die Relationen aufzuladen, bevor die Daten an die Resource übergeben werden.
Passe die index() Methode so an:
1<?php
2
3namespace App\Http\Controllers\Api;
4
5use App\Http\Controllers\Controller;
6use App\Models\Post;
7use App\Http\Resources\PostResource;
8
9class PostController extends Controller
10{
11 public function index()
12 {
13 // Mit with() laden wir den Autor und die Kategorien direkt mit (Eager Loading)
14 $posts = Post::with(['author', 'categories'])
15 ->where('is_published', true)
16 ->latest()
17 ->paginate(10);
18
19 return PostResource::collection($posts);
20 }
21}Das war der Performance-Fix im Backend. Aber unsere API-Resource gibt diese neu geladenen Daten noch gar nicht an das Frontend weiter. Wir müssen unsere JSON-Struktur erweitern.
Schritt 6: Eigene Resources für User und Kategorien generieren
Wir wollen niemals komplette Datenbank-Modelle nackt in unsere API durchwinken. Ein User-Modell enthält sensible Daten wie E-Mail-Adressen, gehashte Passwörter und versteckte Tokens. Wenn wir das einfach ausgeben, reißen wir eine gigantische Sicherheitslücke auf.
Wir bauen strikte Übersetzer für unsere Relationen. Führe im Terminal diese beiden Befehle nacheinander aus:
php artisan make:resource UserResource
php artisan make:resource CategoryResourceÖffne die app/Http/Resources/UserResource.php. Hier bestimmen wir, was die Welt über unsere Autoren wissen darf:
1<?php
2
3namespace App\Http\Resources;
4
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\JsonResource;
7
8class UserResource extends JsonResource
9{
10 public function toArray(Request $request): array
11 {
12 return [
13 'id' => $this->id,
14 'name' => $this->name,
15 // Wir könnten hier später noch einen Link zu einem Avatar-Bild hinzufügen
16 ];
17 }
18}Anschließend konfigurieren wir die app/Http/Resources/CategoryResource.php:
1<?php
2
3namespace App\Http\Resources;
4
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\JsonResource;
7
8class CategoryResource extends JsonResource
9{
10 public function toArray(Request $request): array
11 {
12 return [
13 'id' => $this->id,
14 'name' => $this->name,
15 'slug' => $this->slug,
16 ];
17 }
18}Schritt 7: Die PostResource verknüpfen
Jetzt führen wir alle Fäden zusammen. Öffne unsere bereits bestehende app/Http/Resources/PostResource.php und erweitere das Array.
Wir weisen der Resource an, die geladenen Relationen durch unsere neuen, sicheren Übersetzer zu schicken:
1<?php
2
3namespace App\Http\Resources;
4
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\JsonResource;
7
8class PostResource extends JsonResource
9{
10 public function toArray(Request $request): array
11 {
12 return [
13 'id' => $this->id,
14 'title' => $this->title,
15 'slug' => $this->slug,
16 'published_at' => $this->created_at->format('d.m.Y'),
17 'excerpt' => str()->limit($this->content, 120),
18 'content' => $this->content,
19
20 // Relationen sicher und sauber einbinden
21 'author' => new UserResource($this->whenLoaded('author')),
22 'categories' => CategoryResource::collection($this->whenLoaded('categories')),
23 ];
24 }
25}Ein kleines, aber extrem wichtiges Detail hier ist die Methode whenLoaded(). Sie verhindert, dass die Resource versehentlich eine Datenbankabfrage auslöst, falls wir im Controller einmal vergessen sollten, die Relationen per with() zu laden. In so einem Fall wird der Schlüssel im JSON einfach ignoriert, anstatt den Server zu blockieren.
Wenn du nun deine API-Route (/api/posts) im Browser aufrufst, wirst du ein wunderschön verschachteltes JSON-Konstrukt sehen. Jeder Artikel hat nun ein sicheres author-Objekt und ein Array von categories.
Unser Backend liefert diese hochkomplexen Datenstrukturen jetzt rasend schnell und absolut sicher aus.

Das Laravel Datenbank Design im Frontend zum Leben erwecken
Unser Backend feuert nun hochkomplexe, strukturierte JSON-Pakete in Rekordgeschwindigkeit ab. Aber was nützt die beste unsichtbare Architektur, wenn der Nutzer nichts davon sieht? Stell dir vor, du baust eine gigantische, perfekt sortierte Bibliothek, vergisst aber, die Schilder an die Regale zu hängen.
Ein exzellentes Laravel Datenbank Design erfüllt seinen Zweck erst dann vollständig, wenn das Frontend diese Beziehungen elegant visualisiert. Wir müssen unserem Next.js App Router jetzt beibringen, die neuen Datenpunkte (author und categories) aus der API-Response zu lesen und schicke UI-Elemente daraus zu bauen.
Schritt 8: Die Next.js Startseite aufrüsten
Öffne wieder unser Frontend-Projekt (cms-frontend) und navigiere in die Datei src/app/page.tsx. Wir müssen an unserer Fetch-Logik absolut nichts verändern. Das ist das Schöne an React Server Components gepaart mit unseren Laravel API-Resources: Die neuen Daten fließen bereits ganz automatisch in die posts Variable.
Wir konzentrieren uns rein auf das visuelle Rendering. Suche den HTML-Teil in der map-Schleife, der unsere Artikel-Karte generiert, und tausche ihn gegen diesen erweiterten Code aus:
1posts.map((post: any) => (
2 <article key={post.id} className="bg-zinc-900 border border-zinc-800 p-8 rounded-2xl hover:border-zinc-700 transition-colors">
3
4 {/* Meta-Zeile: Autor und Datum */}
5 <div className="flex items-center gap-3 mb-4 text-sm">
6 <span className="font-semibold text-emerald-400">
7 {post.author?.name || 'Gastautor'}
8 </span>
9 <span className="text-zinc-600">•</span>
10 <span className="text-zinc-400">
11 {post.published_at}
12 </span>
13 </div>
14
15 <h2 className="text-2xl font-bold text-zinc-100 mb-4">
16 {post.title}
17 </h2>
18
19 <p className="text-zinc-400 leading-relaxed mb-6">
20 {post.excerpt}
21 </p>
22
23 {/* Footer-Zeile: Kategorien und Weiterlesen-Link */}
24 <div className="flex justify-between items-center mt-auto pt-4 border-t border-zinc-800">
25
26 {/* Kategorie-Badges */}
27 <div className="flex flex-wrap gap-2">
28 {post.categories?.map((category: any) => (
29 <span
30 key={category.id}
31 className="bg-zinc-950 border border-zinc-800 text-zinc-300 px-3 py-1 rounded-full text-xs font-medium"
32 >
33 {category.name}
34 </span>
35 ))}
36 </div>
37
38 <div className="text-sm text-emerald-500 font-medium hover:text-emerald-400 transition-colors">
39 Artikel lesen →
40 </div>
41 </div>
42
43 </article>
44 ))Speichere die Datei ab und wechsle in deinen Browser (http://localhost:3000).
Siehst du den Unterschied? Aus unserer flachen, langweiligen Liste ist ein echtes, informationsreiches Magazin-Layout geworden. Über jedem Titel prangt nun der Name des Autors in einem satten Smaragdgrün. Unten links reihen sich die sauberen, abgerundeten Badges für unsere Kategorien aneinander.
Da wir im Backend eine Many-to-Many-Beziehung etabliert haben, können hier problemlos Artikel auftauchen, die sowohl den Badge "Tech" als auch "News" tragen. Das Layout skaliert perfekt mit.
Wir haben die Beziehungen zwischen Tabellen, die wir tief im Code geschmiedet haben, nun direkt auf den Bildschirm des Nutzers gebracht.

Das Sicherheitsnetz: Soft Deletes im Laravel Datenbank Design integrieren
Wir alle kennen dieses eiskalte Gefühl im Nacken. Dieser Bruchteil einer Sekunde, nachdem man auf "Löschen" geklickt hat und plötzlich realisiert: Das war der falsche Datensatz. Wenn ein Redakteur in unserem zukünftigen Admin-Dashboard aus Versehen eine Hauptkategorie löscht, an der hunderte Artikel hängen, haben wir ohne ein perfektes Laravel Datenbank Design ein massives Problem. Die Daten wären unwiederbringlich vernichtet.
Hier ziehen wir unsere letzte, absolute Enterprise-Schutzmauer für dieses Kapitel hoch: Soft Deletes.
Anstatt eine Zeile physisch aus der MySQL-Datenbank zu radieren, markiert Laravel den Datensatz lediglich mit einem Zeitstempel (deleted_at). Für das Frontend und unsere Standard-API-Abfragen existiert der Artikel ab diesem Moment nicht mehr. In der Datenbank liegt er jedoch sicher und unangetastet, bereit, jederzeit mit einem einzigen Befehl wiederhergestellt zu werden.
Schritt 9: Die Migration für Soft Deletes anlegen
Lass uns unsere wertvollen Artikel mit diesem Netz absichern. Öffne dein Terminal im cms-backend und erstelle eine neue Änderungs-Migration:
php artisan make:migration add_soft_deletes_to_posts_tableÖffne die frisch generierte Datei im Ordner database/migrations. Laravel macht uns das Hinzufügen extrem leicht. Passe die up() und down() Methoden so an:
1public function up(): void
2{
3 Schema::table('posts', function (Blueprint $table) {
4 // Fügt die 'deleted_at' Spalte (Timestamp) hinzu
5 $table->softDeletes();
6 });
7}
8
9public function down(): void
10{
11 Schema::table('posts', function (Blueprint $table) {
12 // Entfernt die Spalte beim Rollback
13 $table->dropSoftDeletes();
14 });
15}Feuere die Migration in die Datenbank:
php artisan migrateSchritt 10: Das Post-Modell anweisen
Die Tabelle hat nun die Spalte, aber unser PHP-Code ignoriert sie noch. Wir müssen dem Post-Modell explizit sagen, dass es sich ab sofort um einen "weichen" Löschvorgang handeln soll.
Öffne die Datei app/Models/Post.php und importiere den SoftDeletes Trait:
1<?php
2
3namespace App\Models;
4
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\SoftDeletes; // 1. Trait importieren
8
9class Post extends Model
10{
11 use HasFactory, SoftDeletes; // 2. Trait aktivieren
12
13 protected $fillable = [
14 'title',
15 'slug',
16 'content',
17 'is_published',
18 ];
19
20 public function author()
21 {
22 return $this->belongsTo(User::class, 'user_id');
23 }
24
25 public function categories()
26 {
27 return $this->belongsToMany(Category::class);
28 }
29}Das war’s! Wenn du nun in deinem Controller (den wir im Admin-Bereich später bauen) $post->delete() aufrufst, bleibt die Zeile in der Datenbank erhalten.
Das Geniale daran: Unsere öffentliche API (/api/posts), die wir vorhin gebaut haben, blendet diese "gelöschten" Artikel ab sofort völlig automatisch aus. Du musst kein einziges zusätzliches whereNull('deleted_at') in deinen Controller schreiben. Eloquent regelt das unsichtbar im Hintergrund.
Wenn wir später im Admin-Dashboard einen Papierkorb bauen wollen, können wir einfach Post::withTrashed()->get() oder Post::onlyTrashed()->get() aufrufen. Das ist die Macht eines wirklich vorausschauenden Laravel Datenbank Designs.

Zusammenfassung: Dein Laravel Datenbank Design ist bereit für die Skalierung
Wir haben in diesem Kapitel die flache, eindimensionale Ebene verlassen und ein echtes, atmendes Datennetzwerk aufgebaut. Ein exzellentes Laravel Datenbank Design ist das absolute Rückgrat jedes erfolgreichen Headless CMS. Du hast gelernt, wie man Artikel, Kategorien und Autoren über saubere One-to-Many und Many-to-Many Relationen (inklusive Pivot-Tabellen) fehlerfrei miteinander verknüpft.
Wir haben unsere Fabrik (Factories und Seeder) so umprogrammiert, dass sie dieses komplexe Beziehungsgeflecht vollautomatisch mit realistischen Testdaten füllt. Den gefährlichsten Performance-Killer der Backend-Entwicklung – das N+1 Query Problem – haben wir durch gezieltes Eager Loading eliminiert. Zuletzt haben wir mit Soft Deletes ein unsichtbares, rettendes Sicherheitsnetz gespannt und die neuen relationalen Datenpunkte (Autoren und Badges) elegant in unsere Next.js UI integriert. Deine Datenbank ist nun eine Festung.
Teil der Serie
Headless CMS mit Laravel und Next.js
Headless CMS mit Laravel 12 & Next.js: Der ultimative Guide Pillar
Das perfekte Laravel Next.js Setup: Projektstruktur für dein Headless CMS
Datenbank-Design für ein skalierbares Headless CMS
Perfekte RESTful API mit Laravel 12 & API Resources bauen
Sichere Laravel Sanctum Next.js Authentifizierung bauen
Häufig gestellte Fragen (FAQ)
Ausblick auf Teil 3: RESTful API in Perfektion bauen
Die Datenbank steht, die Relationen sind geknüpft und gesichert. Doch aktuell feuern wir unsere Daten noch über rudimentäre Endpunkte in die Welt hinaus. Im nächsten Teil unserer Serie widmen wir uns der absoluten Kür der Backend-Architektur: Der perfekten API.
Wir werden unsere Schnittstellen nach strikten REST-Standards strukturieren. Wie erlauben wir dem Next.js Frontend, Artikel nach einer bestimmten Kategorie zu filtern? Wie bauen wir eine performante Volltextsuche ein? Wir werden saubere Controller-Strukturen etablieren und dynamische Filter-Mechanismen einbauen, sodass dein Frontend die exakte Kontrolle darüber erhält, welche Daten das Backend ausliefern soll. Das wird die Brücke zwischen Laravel und Next.js massiv verstärken.

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


