Eloquent: as consultas que voltam sempre no Laravel 13

As quatro consultas Eloquent que se escrevem todos os dias no Laravel 13: whereNull e whereNotNull, exists, firstOrCreate, contagem e seleção de colunas, com o SQL emitido em cada caso.

Eloquent: as consultas que voltam sempre no Laravel 13
Resposta rápida

Para testar um valor nulo, usa whereNull() e whereNotNull(). Para saber se uma linha existe, exists() em vez de count() > 0: a base devolve um booleano em vez de contar. Numa coleção já carregada, testa isEmpty() e nunca if ($collection), que é sempre verdadeiro.

Há quatro perguntas que voltam em ciclo assim que se escrevem consultas Eloquent: esta coluna está a null, este registo existe, quantos são, e como trazer só as colunas úteis. Este guia trata as quatro, com o SQL realmente emitido e as armadilhas que custam uma consulta a mais.

Este artigo faz parte do percurso Desenvolvimento web. Todos os exemplos foram executados em Laravel 13.30.1 com PHP 8.4.25, sobre uma tabela tasks com as colunas id, title, assignee (nullable), active, priority e as colunas de timestamps, cuja ausência provoca um erro logo no primeiro registo, e cuja conversão em objetos Carbon é automática.

Testar se uma coluna está a null: whereNull e whereNotNull

Em SQL escreve-se IS NULL e IS NOT NULL, porque = NULL nunca devolve verdadeiro. O Eloquent expõe as duas condições sob a forma de métodos:

php
// as tarefas sem responsável
Task::whereNull('assignee')->get();

// as tarefas que têm um
Task::whereNotNull('assignee')->get();

O SQL produzido é exatamente o esperado:

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

O mesmo funciona no construtor de consultas, sem passar por um modelo:

php
use Illuminate\Support\Facades\DB;

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

Podes passar um array para testar várias colunas de uma vez, ficando a condição a incidir sobre todas:

php
Task::whereNull(['assignee', 'closed_at'])->get();
Uma ideia feita a corrigir

Lê-se muitas vezes que where('assignee', null) não funciona. É falso há muito tempo: o Laravel deteta o valor nulo e gera is null. Verificado em Laravel 13, Task::where('assignee', null) produz select * from "tasks" where "assignee" is null e devolve as mesmas linhas que whereNull(). Ainda assim, prefere whereNull(): a intenção fica explícita e não depende dessa normalização.

As variantes seguem a lógica habitual do construtor, com or e a negação:

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

Testar se um registo existe

Para saber se há uma linha correspondente, não vás buscá-la. O exists() pede à base de dados uma resposta booleana:

php
if (Task::where('title', 'Publier')->exists()) {
    // pelo menos uma linha corresponde
}

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

A diferença face ao count() vê-se no log das consultas. Eis o que as três escritas enviam de facto à base:

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

As três respondem à pergunta, mas a terceira traz e hidrata todas as linhas em objetos para deitar fora o conteúdo logo a seguir. Numa tabela volumosa, é a diferença entre uma consulta de custo constante e uma consulta que engorda com os dados.

Criar só se o registo não existir

O par «verifico e depois insiro» tem nome: firstOrCreate(). O primeiro array serve para procurar, o segundo fornece os valores usados apenas em caso de criação.

php
$post = Post::firstOrCreate(
    ['slug' => $slug],                                  // critério de pesquisa
    ['title' => $title, 'body' => $body],               // usado apenas na criação
);

Se a linha já existir, o segundo array é ignorado: o registo é devolvido tal e qual, sem escrita. Duas variantes completam a família:

php
// a mesma lógica, mas sem gravar na base de dados
$post = Post::firstOrNew(['slug' => $slug], ['title' => $title]);
$post->exists;   // false enquanto não chamares save()

// encontra e atualiza, ou cria
$post = Post::updateOrCreate(['slug' => $slug], ['title' => $title]);
Isto não é atómico

O firstOrCreate() executa um SELECT e depois um INSERT. Duas consultas simultâneas podem passar o SELECT ao mesmo tempo e tentar duas inserções. Põe uma restrição de unicidade na base sobre a coluna em causa, através de uma migração: é ela, e não o código PHP, que garante a unicidade.

Contar e detetar uma coleção vazia

É preciso distinguir dois count(). No construtor de consultas, é um COUNT(*) executado pela base de dados. Numa coleção já carregada, é uma simples contagem em memória.

php
// COUNT(*) na base de dados, nenhuma linha trazida
Task::where('active', true)->count();      // 3

// as linhas são carregadas e depois contadas em PHP
$tasks = Task::where('active', true)->get();
$tasks->count();                           // 3

Se já precisas das linhas, a segunda forma é a certa: não lances outra consulta só para contar. Se queres apenas o número, fica-te pela primeira.

Para testar se uma coleção está vazia, há dois métodos explícitos:

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

$tasks->isEmpty();      // true
$tasks->isNotEmpty();   // false
Uma coleção vazia não é falsy

if ($tasks) é sempre verdadeiro, mesmo quando a coleção não contém nada: é um objeto. O teste tem de incidir sobre isEmpty(), isNotEmpty() ou blank(). É o erro mais frequente desta secção.

Repara, já agora, que isEmpty() não recebe argumento nenhum. Encontra-se com frequência $posts->isEmpty($posts), que funciona por acidente, porque o argumento é ignorado.

Numa view Blade, o @forelse trata o caso vazio sem condição explícita:

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

Selecionar só as colunas úteis

Por omissão, o Eloquent faz um select *. Numa tabela larga, ou quando só mostras dois campos, é transferência e hidratação para nada. Três escritas permitem restringir as colunas:

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

// 2. como argumento de get() ou de first()
Task::where('active', true)->get(['title']);
Task::where('assignee', 'Damien')->first(['title', 'priority']);

// 3. acrescentando a uma seleção existente
Task::select('id')->addSelect('title')->first();

Quando queres apenas um valor, dois métodos evitam passar por um modelo completo:

php
// um único valor escalar
Task::where('assignee', 'Damien')->value('title');   // 'Écrire le guide'

// uma lista de valores
Task::where('active', true)->pluck('title');         // ['Écrire le guide', 'Relire']

// uma lista indexada por outra coluna
Task::pluck('title', 'id');                          // [1 => 'Écrire le guide', …]

O value() substitui com vantagem o first(['title'])->title que se escreve por reflexo: devolve diretamente a string, e null se nenhuma linha corresponder, enquanto a outra escrita rebenta com um erro sobre um objeto nulo.

Uma coluna não selecionada não está a null, está ausente

Depois de um first(['title']), o atributo priority não existe no modelo. Aceder-lhe devolve null sem erro, o que esconde o problema até à apresentação. Lembra-te também de incluir a chave primária na seleção: sem id, as relações e o save() deixam de conseguir encontrar a linha.

Erros frequentes

if ($collection) é sempre verdadeiro Uma coleção vazia continua a ser um objeto, logo avaliada como verdadeira. Testa isEmpty(), isNotEmpty() ou blank().
firstOrCreate() não é atómico Um SELECT e depois um INSERT: duas consultas concorrentes podem inserir duas vezes. Acrescenta uma restrição de unicidade na base de dados.
Uma coluna não selecionada está ausente, não está a null Depois de first(['title']), ler priority devolve null sem erro. Inclui sempre a chave primária, senão as relações e o save() deixam de funcionar.
get()->count() traz tudo outra vez Se só precisas do número, usa count() no construtor: é um COUNT(*) que não traz linha nenhuma.
isEmpty() não recebe argumento $posts->isEmpty($posts) vê-se com frequência e funciona por acidente, já que o argumento é ignorado.

EloquentLaravelPHPSQL

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.