Laravel Eloquent: whereNull, exists, count i wybór kolumn

Cztery pytania wracają przy każdym projekcie w Laravelu: czy kolumna jest nullem, czy rekord istnieje, ile ich jest i jak pobrać tylko potrzebne kolumny. Przewodnik po whereNull, exists, firstOrCreate, count i wyborze kolumn, z SQL-em wysyłanym do bazy.

Laravel Eloquent: whereNull, exists, count i wybór kolumn
Szybka odpowiedź

Do sprawdzenia wartości null użyj whereNull() i whereNotNull(). Żeby dowiedzieć się, czy wiersz istnieje, sięgnij po exists() zamiast count() > 0: baza zwraca wartość logiczną, zamiast liczyć. Na już załadowanej kolekcji testuj isEmpty(), nigdy if ($collection), które zawsze jest prawdziwe.

Cztery pytania wracają w kółko, gdy tylko zaczynasz pisać zapytania Eloquent: czy ta kolumna jest nullem, czy ten rekord istnieje, ile ich jest i jak pobrać wyłącznie potrzebne kolumny. Ten przewodnik odpowiada na wszystkie cztery, z SQL-em faktycznie wysyłanym do bazy i pułapkami, które kosztują jedno zapytanie za dużo.

Ten artykuł należy do ścieżki Programowanie webowe. Wszystkie przykłady uruchomiono na Laravelu 13.30.1 z PHP 8.4.25, na tabeli tasks z kolumnami id, title, assignee (nullable), active, priority oraz kolumnami timestampów, których brak wywołuje błąd przy pierwszym zapisie, a których konwersja na obiekty Carbon dzieje się automatycznie.

Sprawdzanie, czy kolumna jest nullem: whereNull i whereNotNull

W SQL-u pisze się IS NULL i IS NOT NULL, bo = NULL nigdy nie zwraca prawdy. Eloquent wystawia oba warunki jako metody:

php
// zadania bez osoby odpowiedzialnej
Task::whereNull('assignee')->get();

// zadania, które go mają
Task::whereNotNull('assignee')->get();

Wygenerowany SQL jest dokładnie taki, jakiego się spodziewasz:

sql
select * from "tasks" where "assignee" is not null

To samo działa na query builderze, bez pośrednictwa modelu:

php
use Illuminate\Support\Facades\DB;

DB::table('tasks')->whereNull('assignee')->get();

Możesz przekazać tablicę, żeby sprawdzić kilka kolumn naraz, warunek obejmie wtedy wszystkie:

php
Task::whereNull(['assignee', 'closed_at'])->get();
Mit, który warto obalić

Często można przeczytać, że where('assignee', null) nie działa. To nieprawda, i to od dawna: Laravel wykrywa wartość null i generuje is null. Sprawdzone na Laravelu 13, Task::where('assignee', null) daje select * from "tasks" where "assignee" is null i zwraca te same wiersze co whereNull(). Mimo to sięgaj po whereNull(): intencja jest jawna i nie zależy od tej normalizacji.

Warianty trzymają się zwykłej logiki query buildera, z or i z negacją:

php
Task::whereNull('assignee')->orWhereNotNull('closed_at')->get();

Sprawdzanie, czy rekord istnieje

Żeby dowiedzieć się, czy jakiś wiersz pasuje, nie pobieraj go. exists() prosi bazę o odpowiedź logiczną:

php
if (Task::where('title', 'Publier')->exists()) {
    // co najmniej jeden wiersz pasuje
}

if (Task::where('title', 'Publier')->doesntExist()) {
    // żadnego wiersza
}

Różnicę wobec count() widać w logu zapytań. Oto co te trzy zapisy naprawdę wysyłają do bazy:

sql
-- 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" = ?

Wszystkie trzy odpowiadają na to pytanie, ale trzeci ściąga wszystkie wiersze i hydratuje je do obiektów, żeby chwilę później wyrzucić ich zawartość. Na dużej tabeli to różnica między zapytaniem o stałym koszcie a zapytaniem, które rośnie razem z danymi.

Tworzenie tylko wtedy, gdy rekordu jeszcze nie ma

Para „najpierw sprawdzam, potem wstawiam” ma swoją nazwę: firstOrCreate(). Pierwsza tablica służy do szukania, druga podaje wartości używane wyłącznie przy tworzeniu.

php
$post = Post::firstOrCreate(
    ['slug' => $slug],                                  // kryterium wyszukiwania
    ['title' => $title, 'body' => $body],               // używane wyłącznie przy tworzeniu
);

Jeśli wiersz już istnieje, druga tablica jest ignorowana: rekord wraca bez zmian, bez żadnego zapisu. Rodzinę uzupełniają dwa warianty:

php
// ta sama logika, ale bez zapisu w bazie
$post = Post::firstOrNew(['slug' => $slug], ['title' => $title]);
$post->exists;   // false, dopóki nie wywołasz save()

// znajduje i aktualizuje albo tworzy
$post = Post::updateOrCreate(['slug' => $slug], ['title' => $title]);
To nie jest atomowe

firstOrCreate() wykonuje SELECT, a potem INSERT. Dwa równoległe żądania mogą przejść przez SELECT w tej samej chwili i spróbować wstawić wiersz dwa razy. Załóż w bazie ograniczenie unikalności na tej kolumnie, przez migrację: to ono, a nie kod PHP, gwarantuje unikalność.

Zliczanie i wykrywanie pustej kolekcji

Trzeba rozróżnić dwa count(). Na query builderze to COUNT(*) wykonany przez bazę. Na już załadowanej kolekcji to zwykłe zliczenie w pamięci.

php
// COUNT(*) w bazie, żaden wiersz nie jest pobierany
Task::where('active', true)->count();      // 3

// wiersze są ładowane, a potem liczone w PHP
$tasks = Task::where('active', true)->get();
$tasks->count();                           // 3

Jeśli i tak potrzebujesz wierszy, właściwa jest druga forma: nie odpalaj osobnego zapytania zliczającego. Jeśli chcesz samej liczby, weź pierwszą.

Do sprawdzenia, czy kolekcja jest pusta, służą dwie jawne metody:

php
$tasks = Task::where('title', 'Inexistant')->get();

$tasks->isEmpty();      // true
$tasks->isNotEmpty();   // false
Pusta kolekcja nie jest falsy

if ($tasks) jest zawsze prawdziwe, nawet gdy kolekcja nic nie zawiera: to obiekt. Test musi opierać się na isEmpty(), isNotEmpty() albo blank(). To najczęstszy błąd w tej sekcji.

Przy okazji: isEmpty() nie przyjmuje żadnego argumentu. Często widuje się $posts->isEmpty($posts), które działa przez przypadek, bo argument i tak jest ignorowany.

W widoku Blade @forelse obsługuje pusty przypadek bez jawnego warunku:

resources/views/tasks/index.blade.php
@forelse ($tasks as $task)
    <li>{{ $task->title }}</li>
@empty
    <li>Aucune tâche pour le moment.</li>
@endforelse

Pobieranie tylko potrzebnych kolumn

Domyślnie Eloquent robi select *. Na szerokiej tabeli albo wtedy, gdy wyświetlasz dwa pola, to transfer i hydratacja na darmo. Kolumny da się ograniczyć na trzy sposoby:

php
// 1. select() na query builderze
Task::select('title', 'priority')->where('active', true)->get();

// 2. jako argument get() albo first()
Task::where('active', true)->get(['title']);
Task::where('assignee', 'Damien')->first(['title', 'priority']);

// 3. przez dodanie do istniejącego wyboru
Task::select('id')->addSelect('title')->first();

Kiedy potrzebujesz jednej wartości, dwie metody oszczędzają ci budowania pełnego modelu:

php
// pojedyncza wartość skalarna
Task::where('assignee', 'Damien')->value('title');   // 'Écrire le guide'

// lista wartości
Task::where('active', true)->pluck('title');         // ['Écrire le guide', 'Relire']

// lista indeksowana inną kolumną
Task::pluck('title', 'id');                          // [1 => 'Écrire le guide', …]

value() z powodzeniem zastępuje odruchowo pisane first(['title'])->title: zwraca od razu łańcuch znaków, a null, kiedy żaden wiersz nie pasuje, podczas gdy ten drugi zapis wywala się na pustym obiekcie.

Niewybrana kolumna nie jest nullem, po prostu jej nie ma

Po first(['title']) atrybut priority nie istnieje na modelu. Odwołanie do niego zwraca null bez błędu, co ukrywa problem aż do wyświetlenia. Pamiętaj też o kluczu głównym w wyborze kolumn: bez id relacje i save() nie potrafią już odnaleźć wiersza.

Częste błędy

if ($collection) jest zawsze prawdziwe Pusta kolekcja pozostaje obiektem, więc wypada jako prawda. Testuj isEmpty(), isNotEmpty() albo blank().
firstOrCreate() nie jest atomowe Najpierw SELECT, potem INSERT: dwa równoległe żądania mogą wstawić wiersz dwa razy. Dodaj w bazie ograniczenie unikalności.
Niewybranej kolumny nie ma, nie jest nullem Po first(['title']) odczyt priority zwraca null bez błędu. Zawsze dołączaj klucz główny, inaczej relacje i save() przestają działać.
get()->count() ściąga wszystko Jeśli potrzebujesz tylko liczby, użyj count() na query builderze: to COUNT(*), które nie pobiera żadnego wiersza.
isEmpty() nie przyjmuje argumentu $posts->isEmpty($posts) widuje się często i działa przez przypadek, bo argument jest ignorowany.

EloquentLaravelPHPSQL

Damien Flandrin Web developer od 2010 roku, twórca Gekkode i Email Impact. Każdy artykuł jest sprawdzany na prawdziwym projekcie przed publikacją. Kontakt
Newsletter

Nowe testy, poradniki i projekty — e-mailem.

Powtarzalne testy, wersjonowany kod, datowane wyniki. Nigdy spamu.