Query Eloquent: null, esistenza, conteggio e colonne

L'ORM Eloquent incluso in Laravel offre un modo semplice di dialogare con il database: semplifica tutte le operazioni CRUD e qualsiasi altra query. In questa guida vedi come testare un valore nullo, come sapere se un record esiste, come contare e come selezionare solo le colonne che ti servono.

Query Eloquent: null, esistenza, conteggio e colonne
Risposta rapida

Per testare un valore nullo usa whereNull() e whereNotNull(). Per sapere se una riga esiste, exists() invece di count() > 0: il database restituisce un booleano invece di contare. Su una collection già caricata usa isEmpty() e mai if ($collection), che è sempre vero.

Quattro domande tornano in continuazione appena si scrivono query Eloquent: questa colonna è nulla, questo record esiste, quanti ce ne sono, e come recuperare solo le colonne che servono davvero. Questa guida le affronta tutte e quattro, con il SQL realmente prodotto e le trappole che costano una query di troppo.

Questo articolo fa parte del percorso Sviluppo web. Tutti gli esempi sono stati eseguiti su Laravel 13.30.1 con PHP 8.4.25, su una tabella tasks che contiene le colonne id, title, assignee (nullable), active, priority e le colonne di timestamp, la cui assenza provoca un errore al primo salvataggio, e la cui conversione in oggetti Carbon è automatica.

Verificare se una colonna è nulla: whereNull e whereNotNull

In SQL si scrive IS NULL e IS NOT NULL, perché = NULL non è mai vero. Eloquent espone le due condizioni sotto forma di metodi:

php
// le task senza responsabile
Task::whereNull('assignee')->get();

// le task che ne hanno uno
Task::whereNotNull('assignee')->get();

Il SQL prodotto è esattamente quello che ci si aspetta:

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

La stessa cosa funziona sul query builder, senza passare da un modello:

php
use Illuminate\Support\Facades\DB;

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

Puoi passare un array per testare più colonne insieme: la condizione vale allora su tutte.

php
Task::whereNull(['assignee', 'closed_at'])->get();
Una falsa credenza da correggere

Si legge spesso che where('assignee', null) non funziona. È falso da parecchio tempo: Laravel riconosce il valore nullo e genera is null. Verificato su Laravel 13, Task::where('assignee', null) produce select * from "tasks" where "assignee" is null e restituisce le stesse righe di whereNull(). Meglio comunque whereNull(): l’intenzione è esplicita e non dipende da questa normalizzazione.

Le varianti seguono la logica abituale del query builder, con or e la negazione:

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

Verificare se un record esiste

Per sapere se esiste una riga corrispondente, non andarla a caricare. exists() chiede al database una risposta booleana:

php
if (Task::where('title', 'Publier')->exists()) {
    // almeno una riga corrisponde
}

if (Task::where('title', 'Publier')->doesntExist()) {
    // nessuna riga
}

La differenza rispetto a count() si vede nel log delle query. Ecco che cosa mandano davvero al database le tre scritture:

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

Tutte e tre rispondono alla domanda, ma la terza carica e idrata tutte le righe in oggetti per buttarne via subito il contenuto. Su una tabella grande è la differenza fra una query a costo costante e una query che cresce insieme ai dati.

Creare solo se il record non esiste

La coppia « prima controllo, poi inserisco » ha un nome: firstOrCreate(). Il primo array serve a cercare, il secondo fornisce i valori usati soltanto in caso di creazione.

php
$post = Post::firstOrCreate(
    ['slug' => $slug],                                  // criterio di ricerca
    ['title' => $title, 'body' => $body],               // usato solo in fase di creazione
);

Se la riga esiste già, il secondo array viene ignorato: il record viene restituito così com’è, senza scrittura. Due varianti completano la famiglia:

php
// stessa logica, ma senza salvare nel database
$post = Post::firstOrNew(['slug' => $slug], ['title' => $title]);
$post->exists;   // false finché non chiami save()

// trova e aggiorna, oppure crea
$post = Post::updateOrCreate(['slug' => $slug], ['title' => $title]);
Non è atomico

firstOrCreate() esegue un SELECT e poi un INSERT. Due richieste simultanee possono superare il SELECT nello stesso momento e tentare due inserimenti. Metti un vincolo di unicità sulla colonna interessata, con una migration: è quello, e non il codice PHP, a garantire l’unicità.

Contare e riconoscere una collection vuota

Bisogna distinguere due count(). Sul query builder è un COUNT(*) eseguito dal database. Su una collection già caricata è un semplice conteggio in memoria.

php
// COUNT(*) nel database, nessuna riga caricata
Task::where('active', true)->count();      // 3

// le righe vengono caricate, poi contate in PHP
$tasks = Task::where('active', true)->get();
$tasks->count();                           // 3

Se ti servono comunque le righe, la seconda forma è quella giusta: non lanciare una query di conteggio in più. Se vuoi solo il numero, prendi la prima.

Per capire se una collection è vuota ci sono due metodi espliciti:

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

$tasks->isEmpty();      // true
$tasks->isNotEmpty();   // false
Una collection vuota non è falsy

if ($tasks) è sempre vero, anche quando la collection non contiene niente: è un oggetto. Il test va fatto con isEmpty(), isNotEmpty() o blank(). È l’errore più frequente di questa sezione.

Nota di passaggio che isEmpty() non accetta argomenti. Si incontra spesso $posts->isEmpty($posts), che funziona per caso perché l’argomento viene ignorato.

In una vista Blade, @forelse gestisce il caso vuoto senza condizione esplicita:

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

Selezionare solo le colonne utili

Per impostazione predefinita Eloquent fa un select *. Su una tabella larga, o quando mostri solo due campi, sono trasferimento e idratazione buttati via. Tre scritture permettono di restringere le colonne:

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

// 2. come argomento di get() o di first()
Task::where('active', true)->get(['title']);
Task::where('assignee', 'Damien')->first(['title', 'priority']);

// 3. aggiungendo a una selezione esistente
Task::select('id')->addSelect('title')->first();

Quando ti serve un valore soltanto, due metodi evitano di passare da un modello completo:

php
// un solo valore scalare
Task::where('assignee', 'Damien')->value('title');   // 'Écrire le guide'

// un elenco di valori
Task::where('active', true)->pluck('title');         // ['Écrire le guide', 'Relire']

// un elenco indicizzato da un'altra colonna
Task::pluck('title', 'id');                          // [1 => 'Écrire le guide', …]

value() sostituisce con vantaggio il first(['title'])->title che si scrive per riflesso: restituisce direttamente la stringa, e null se nessuna riga corrisponde, là dove l’altra scrittura provoca un errore su un oggetto nullo.

Una colonna non selezionata non è nulla, è assente

Dopo un first(['title']), l’attributo priority non esiste sul modello. Leggerlo restituisce null senza errore, e il problema resta nascosto fino alla visualizzazione. Ricordati anche di includere la chiave primaria nella selezione: senza id, le relazioni e save() non riescono più a ritrovare la riga.

Errori frequenti

if ($collection) è sempre vero Una collection vuota resta un oggetto, quindi viene valutata come vera. Usa isEmpty(), isNotEmpty() o blank().
firstOrCreate() non è atomica Un SELECT e poi un INSERT: due richieste concorrenti possono inserire due volte. Aggiungi un vincolo di unicità nel database.
Una colonna esclusa dalla select è assente, non nulla Dopo first(['title']), leggere priority restituisce null senza errore. Includi sempre la chiave primaria, altrimenti relazioni e save() smettono di funzionare.
get()->count() carica tutto Se ti serve solo il numero, chiama count() sul builder: è un COUNT(*) che non porta indietro nessuna riga.
isEmpty() non accetta argomenti $posts->isEmpty($posts) si vede spesso e funziona per caso, dato che l'argomento viene ignorato.

EloquentLaravelPHPSQL

Damien Flandrin Sviluppatore web dal 2010, creatore di Gekkode e di Email Impact. Ogni articolo è testato su un progetto reale prima della pubblicazione. Contatti
Newsletter

I nuovi test, tutorial e progetti, via e-mail.

Test riproducibili, codice versionato, risultati datati. Mai spam.