
Gebruik whereNull() en whereNotNull() om op een null-waarde te testen. Wil je weten of een rij bestaat, neem dan exists() in plaats van count() > 0: de database geeft een boolean terug in plaats van te tellen. Test op een al geladen collectie met isEmpty(), en nooit met if ($collection), dat altijd waar is.
Vier vragen komen steeds terug zodra je Eloquent-queries schrijft: is deze kolom null, bestaat dit record, hoeveel zijn het er, en hoe haal je alleen de kolommen op die je nodig hebt. Deze gids behandelt alle vier, met het SQL dat er werkelijk uit gaat en de valkuilen die je een query te veel kosten.
Dit artikel maakt deel uit van het leerpad Webdevelopment. Alle voorbeelden draaiden op Laravel 13.30.1 met PHP 8.4.25, op een tabel tasks met de kolommen id, title, assignee (nullable), active, priority en de timestampkolommen, waarvan het ontbreken bij de eerste save een fout oplevert, en waarvan de omzetting naar Carbon-objecten automatisch gaat.
Testen of een kolom null is: whereNull en whereNotNull
In SQL schrijf je IS NULL en IS NOT NULL, omdat = NULL nooit waar oplevert. Eloquent biedt beide condities als methode aan:
// de taken zonder verantwoordelijke
Task::whereNull('assignee')->get();
// de taken die er een hebben
Task::whereNotNull('assignee')->get();Het geproduceerde SQL is precies wat je verwacht:
select * from "tasks" where "assignee" is not nullHetzelfde werkt op de query builder, zonder tussenkomst van een model:
use Illuminate\Support\Facades\DB;
DB::table('tasks')->whereNull('assignee')->get();Je kunt een array meegeven om meerdere kolommen tegelijk te testen, de conditie geldt dan voor alle kolommen:
Task::whereNull(['assignee', 'closed_at'])->get();Je leest vaak dat where('assignee', null) niet werkt. Dat klopt al lang niet meer: Laravel herkent de null-waarde en genereert is null. Gecontroleerd op Laravel 13 levert Task::where('assignee', null) het SQL select * from "tasks" where "assignee" is null op en dezelfde rijen als whereNull(). Kies toch voor whereNull(): de bedoeling is expliciet en hangt niet van die normalisatie af.
De varianten volgen de gebruikelijke logica van de builder, met or en de ontkenning:
Task::whereNull('assignee')->orWhereNotNull('closed_at')->get();Testen of een record bestaat
Wil je weten of er een rij voldoet, ga hem dan niet ophalen. exists() vraagt de database om een booleaans antwoord:
if (Task::where('title', 'Publier')->exists()) {
// minstens één rij voldoet
}
if (Task::where('title', 'Publier')->doesntExist()) {
// geen enkele rij
}Het verschil met count() zie je in de query log. Dit is wat de drie schrijfwijzen werkelijk naar de database sturen:
-- 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 drie beantwoorden ze de vraag, maar de derde haalt alle rijen op en hydrateert ze tot objecten om de inhoud meteen weer weg te gooien. Op een grote tabel is dat het verschil tussen een constante query en een query die meegroeit met de data.
Alleen aanmaken als het record nog niet bestaat
Het duo ‘eerst controleren, dan invoegen’ heeft een naam: firstOrCreate(). De eerste array dient om te zoeken, de tweede levert de waarden die alleen bij het aanmaken worden gebruikt.
$post = Post::firstOrCreate(
['slug' => $slug], // zoekcriterium
['title' => $title, 'body' => $body], // alleen gebruikt bij het aanmaken
);Bestaat de rij al, dan wordt de tweede array genegeerd: het record komt ongewijzigd terug, zonder schrijfactie. Twee varianten maken de familie compleet:
// dezelfde logica, maar zonder op te slaan in de database
$post = Post::firstOrNew(['slug' => $slug], ['title' => $title]);
$post->exists; // false zolang je save() niet aanroept
// vindt en werkt bij, of maakt aan
$post = Post::updateOrCreate(['slug' => $slug], ['title' => $title]);firstOrCreate() voert een SELECT uit en daarna een INSERT. Twee gelijktijdige requests kunnen tegelijk door de SELECT komen en allebei een insert proberen. Leg een unieke constraint op de betreffende kolom in de database, via een migratie: die garandeert de uniciteit, en niet de PHP-code.
Tellen en een lege collectie herkennen
Je moet twee soorten count() uit elkaar houden. Op de query builder is het een COUNT(*) die de database uitvoert. Op een al geladen collectie is het gewoon tellen in het geheugen.
// COUNT(*) in de database, geen enkele rij opgehaald
Task::where('active', true)->count(); // 3
// de rijen worden geladen en daarna in PHP geteld
$tasks = Task::where('active', true)->get();
$tasks->count(); // 3Heb je de rijen toch al nodig, dan is de tweede vorm de juiste: start geen tweede query om te tellen. Wil je alleen het aantal, neem dan de eerste.
Om te testen of een collectie leeg is, zijn er twee expliciete methoden:
$tasks = Task::where('title', 'Inexistant')->get();
$tasks->isEmpty(); // true
$tasks->isNotEmpty(); // falseif ($tasks) is altijd waar, ook als de collectie niets bevat: het is een object. De test hoort op isEmpty(), isNotEmpty() of blank() te staan. Dit is de meest voorkomende fout uit deze sectie.
Merk en passant op dat isEmpty() geen enkel argument aanneemt. Je ziet vaak $posts->isEmpty($posts), wat per ongeluk werkt omdat het argument wordt genegeerd.
In een Blade-view handelt @forelse het lege geval af zonder expliciete conditie:
@forelse ($tasks as $task)
<li>{{ $task->title }}</li>
@empty
<li>Aucune tâche pour le moment.</li>
@endforelseAlleen de kolommen selecteren die je nodig hebt
Standaard doet Eloquent een select *. Op een brede tabel, of wanneer je maar twee velden toont, is dat overbodig transport en overbodige hydratatie. Drie schrijfwijzen beperken de kolommen:
// 1. select() op de builder
Task::select('title', 'priority')->where('active', true)->get();
// 2. als argument van get() of first()
Task::where('active', true)->get(['title']);
Task::where('assignee', 'Damien')->first(['title', 'priority']);
// 3. door toe te voegen aan een bestaande selectie
Task::select('id')->addSelect('title')->first();Wil je maar één waarde, dan voorkomen twee methoden dat je een volledig model ophaalt:
// één enkele scalaire waarde
Task::where('assignee', 'Damien')->value('title'); // 'Écrire le guide'
// een lijst met waarden
Task::where('active', true)->pluck('title'); // ['Écrire le guide', 'Relire']
// een lijst geïndexeerd op een andere kolom
Task::pluck('title', 'id'); // [1 => 'Écrire le guide', …]value() is een betere keuze dan de first(['title'])->title die je uit gewoonte schrijft: het geeft direct de string terug, en null als geen enkele rij voldoet, terwijl de andere schrijfwijze een fout geeft op een null-object.
Na een first(['title']) bestaat het attribuut priority niet op het model. Het uitlezen geeft null zonder fout, waardoor het probleem pas bij de weergave opvalt. Denk er ook aan de primaire sleutel in je selectie op te nemen: zonder id kunnen de relaties en save() de rij niet meer terugvinden.
Veelgemaakte fouten
isEmpty(), isNotEmpty() of blank().nfirstOrCreate() is niet atomair | Eerst een SELECT, dan een INSERT: twee gelijktijdige requests kunnen twee keer invoegen. Voeg een unieke constraint toe in de database.first(['title']) geeft het uitlezen van priority null zonder fout. Neem altijd de primaire sleutel mee, anders werken de relaties en save() niet meer.nget()->count() haalt alles opnieuw op | Heb je alleen het aantal nodig, gebruik dan count() op de builder: dat is een COUNT(*) die geen enkele rij ophaalt.nisEmpty() neemt geen argument | $posts->isEmpty($posts) zie je vaak, het werkt per ongeluk, omdat het argument wordt genegeerd.

