
Für einen Nullwert nimmst du whereNull() und whereNotNull(). Ob eine Zeile existiert, klärt exists() statt count() > 0: Die Datenbank liefert einen booleschen Wert, statt zu zählen. Auf einer bereits geladenen Collection prüfst du isEmpty() und niemals if ($collection), was immer wahr ist.
Vier Fragen tauchen beim Schreiben von Eloquent-Abfragen immer wieder auf: Ist diese Spalte null, existiert dieser Datensatz, wie viele sind es, und wie holt man nur die Spalten, die man wirklich braucht? Dieser Leitfaden behandelt alle vier, mit dem tatsächlich abgesetzten SQL und den Fallen, die eine Abfrage zu viel kosten.
Dieser Artikel gehört zum Lernpfad Webentwicklung. Alle Beispiele liefen auf Laravel 13.30.1 mit PHP 8.4.25, auf einer Tabelle tasks mit den Spalten id, title, assignee (nullable), active, priority und den Timestamp-Spalten, deren Fehlen beim ersten Speichern einen Fehler auslöst und deren Umwandlung in Carbon-Objekte automatisch geschieht.
Prüfen, ob eine Spalte null ist: whereNull und whereNotNull
In SQL schreibt man IS NULL und IS NOT NULL, weil = NULL nie wahr ergibt. Eloquent stellt beide Bedingungen als Methoden bereit:
// die Aufgaben ohne Verantwortlichen
Task::whereNull('assignee')->get();
// die Aufgaben, die einen haben
Task::whereNotNull('assignee')->get();Das erzeugte SQL ist genau das erwartete:
select * from "tasks" where "assignee" is not nullDasselbe funktioniert auf dem Query Builder, ganz ohne Modell:
use Illuminate\Support\Facades\DB;
DB::table('tasks')->whereNull('assignee')->get();Du kannst ein Array übergeben, um mehrere Spalten auf einmal zu prüfen, die Bedingung gilt dann für alle:
Task::whereNull(['assignee', 'closed_at'])->get();Oft liest man, where('assignee', null) funktioniere nicht. Das ist seit Langem falsch: Laravel erkennt den Nullwert und erzeugt is null. Auf Laravel 13 geprüft, ergibt Task::where('assignee', null) genau select * from "tasks" where "assignee" is null und liefert dieselben Zeilen wie whereNull(). Nimm trotzdem lieber whereNull(): Die Absicht steht damit ausdrücklich im Code und hängt nicht an dieser Normalisierung.
Die Varianten folgen der gewohnten Logik des Builders, mit or und der Negation:
Task::whereNull('assignee')->orWhereNotNull('closed_at')->get();Prüfen, ob ein Datensatz existiert
Um zu wissen, ob eine Zeile passt, musst du sie nicht holen. exists() lässt die Datenbank eine boolesche Antwort liefern:
if (Task::where('title', 'Publier')->exists()) {
// mindestens eine Zeile passt
}
if (Task::where('title', 'Publier')->doesntExist()) {
// keine Zeile
}Der Unterschied zu count() zeigt sich im Query-Log. Das schicken die drei Schreibweisen tatsächlich an die Datenbank:
-- exists()
select exists(select * from "tasks" where "active" = ?) as "exists"
-- count() > 0
select count(*) as "aggregate" from "tasks" where "active" = ?
-- get()->isNotEmpty()
select * from "tasks" where "active" = ?Alle drei beantworten die Frage, aber die dritte holt sämtliche Zeilen und hydriert sie zu Objekten, nur um den Inhalt sofort wegzuwerfen. Auf einer großen Tabelle ist das der Unterschied zwischen einer Abfrage mit konstanten Kosten und einer, die mit den Daten mitwächst.
Nur anlegen, wenn der Datensatz nicht existiert
Das Paar „erst prüfen, dann einfügen“ hat einen Namen: firstOrCreate(). Das erste Array dient der Suche, das zweite liefert die Werte, die nur beim Anlegen verwendet werden.
$post = Post::firstOrCreate(
['slug' => $slug], // Suchkriterium
['title' => $title, 'body' => $body], // nur beim Anlegen verwendet
);Existiert die Zeile bereits, wird das zweite Array ignoriert: Der Datensatz kommt unverändert zurück, ohne Schreibzugriff. Zwei Varianten ergänzen die Familie:
// gleiche Logik, aber ohne in der Datenbank zu speichern
$post = Post::firstOrNew(['slug' => $slug], ['title' => $title]);
$post->exists; // false, solange du save() nicht aufrufst
// findet und aktualisiert, oder legt an
$post = Post::updateOrCreate(['slug' => $slug], ['title' => $title]);firstOrCreate() führt ein SELECT und danach ein INSERT aus. Zwei gleichzeitige Anfragen können das SELECT zur selben Zeit passieren und beide einfügen wollen. Setz über eine Migration einen Unique-Constraint auf die betroffene Spalte: Er garantiert die Eindeutigkeit, nicht der PHP-Code.
Zählen und eine leere Collection erkennen
Es gibt zwei count(), die du auseinanderhalten musst. Auf dem Query Builder ist es ein COUNT(*), das die Datenbank ausführt. Auf einer bereits geladenen Collection ist es schlicht ein Zählen im Speicher.
// COUNT(*) in der Datenbank, keine Zeile wird geladen
Task::where('active', true)->count(); // 3
// die Zeilen werden geladen und danach in PHP gezählt
$tasks = Task::where('active', true)->get();
$tasks->count(); // 3Brauchst du die Zeilen ohnehin, ist die zweite Form die richtige: Setz keine zusätzliche Zählabfrage ab. Willst du nur die Anzahl, nimm die erste.
Ob eine Collection leer ist, prüfen zwei ausdrückliche Methoden:
$tasks = Task::where('title', 'Inexistant')->get();
$tasks->isEmpty(); // true
$tasks->isNotEmpty(); // falseif ($tasks) ist immer wahr, auch wenn die Collection nichts enthält: Sie ist ein Objekt. Der Test muss über isEmpty(), isNotEmpty() oder blank() laufen. Das ist der häufigste Fehler in diesem Abschnitt.
Nebenbei: isEmpty() nimmt kein Argument entgegen. Man sieht häufig $posts->isEmpty($posts), was nur zufällig funktioniert, weil das Argument ignoriert wird.
In einer Blade-View erledigt @forelse den leeren Fall ohne ausdrückliche Bedingung:
@forelse ($tasks as $task)
<li>{{ $task->title }}</li>
@empty
<li>Aucune tâche pour le moment.</li>
@endforelseNur die nötigen Spalten auswählen
Standardmäßig macht Eloquent ein select *. Bei einer breiten Tabelle oder wenn du nur zwei Felder anzeigst, sind das Übertragung und Hydration für nichts. Drei Schreibweisen schränken die Spalten ein:
// 1. select() auf dem Query Builder
Task::select('title', 'priority')->where('active', true)->get();
// 2. als Argument von get() oder first()
Task::where('active', true)->get(['title']);
Task::where('assignee', 'Damien')->first(['title', 'priority']);
// 3. eine bestehende Auswahl ergänzen
Task::select('id')->addSelect('title')->first();Wenn du nur einen einzigen Wert brauchst, ersparen dir zwei Methoden den Umweg über ein vollständiges Modell:
// ein einzelner Skalarwert
Task::where('assignee', 'Damien')->value('title'); // 'Écrire le guide'
// eine Liste von Werten
Task::where('active', true)->pluck('title'); // ['Écrire le guide', 'Relire']
// eine Liste, indexiert über eine andere Spalte
Task::pluck('title', 'id'); // [1 => 'Écrire le guide', …]value() ersetzt das reflexhaft geschriebene first(['title'])->title mit Gewinn: Es gibt direkt den String zurück und null, wenn keine Zeile passt, während die andere Schreibweise auf einem Nullobjekt einen Fehler auslöst.
Nach einem first(['title']) gibt es das Attribut priority auf dem Modell nicht. Der Zugriff darauf liefert null ohne Fehler, und das Problem bleibt bis zur Anzeige verborgen. Nimm außerdem den Primärschlüssel in deine Auswahl auf: ohne id finden die Relationen und save() die Zeile nicht mehr wieder.
Häufige Fehler
isEmpty(), isNotEmpty() oder blank().nfirstOrCreate() ist nicht atomar | Ein SELECT, danach ein INSERT: Zwei gleichzeitige Anfragen können zweimal einfügen. Ergänze einen Unique-Constraint in der Datenbank.first(['title']) liefert das Lesen von priority null ohne Fehler. Nimm immer den Primärschlüssel mit auf, sonst funktionieren Relationen und save() nicht mehr.nget()->count() holt alles | Brauchst du nur die Anzahl, nutze count() auf dem Builder: ein COUNT(*), das keine einzige Zeile überträgt.nisEmpty() nimmt kein Argument | $posts->isEmpty($posts) sieht man oft, es funktioniert nur zufällig, weil das Argument ignoriert wird.

