Docs›Banco de dados›Query Builder fluente
Banco de dados

Query Builder fluente

where().orderBy().limit().get() encadeado — Eloquent Builder nativo.

O Query Builder do MAD é o Eloquent Builder nativo do Laravel — encadeamento de métodos (where, orderBy, limit, get) chamados direto na classe do model. Não existe camada própria por cima: tudo aqui é Eloquent puro.

Encadeamento básico

$produtos = Produto::where('ativo', true)
    ->where('valor', '>=', 100)
    ->orderBy('nome', 'asc')
    ->limit(50)
    ->get();

Operadores

OperadorUso
=where('campo', $valor) — = é o default, pode omitir
!=, <>where('campo', '!=', $valor)
>, <, >=, <=Comparação numérica/data
likewhere('campo', 'like', "%{$busca}%")
in / not inwhereIn('campo', $array) / whereNotIn(...)
is nullwhereNull('campo') / whereNotNull('campo')
betweenwhereBetween('campo', [$ini, $fim])

OR e agrupamento

// AND (default)
Produto::where('ativo', true)->where('tipo', 'A');
// → WHERE ativo = 1 AND tipo = 'A'

// OR
Produto::where('tipo', 'A')->orWhere('tipo', 'B');
// → WHERE tipo = 'A' OR tipo = 'B'

// Agrupamento — closure isola os parênteses
Produto::where('ativo', true)
    ->where(function ($q) {
        $q->where('tipo', 'A')->orWhere('tipo', 'B');
    })->get();
// → WHERE ativo = 1 AND (tipo = 'A' OR tipo = 'B')

Exemplo real do projeto (App\Models\Sys\ImportTemplate) combinando agrupamento e subquery:

return self::where('active', 'Y')
    ->where(function ($q) use ($groupIds, $userId) {
        if (!empty($groupIds)) {
            $q->whereIn('id', ImportTemplateGroup::select('template_id')
                ->whereIn('group_id', $groupIds));
        }
        $q->orWhereIn('id', ImportTemplateUser::select('template_id')
            ->where('user_id', $userId));
    })->get()->all();

Métodos terminais (executam a query)

MétodoRetornoDescrição
get()CollectionColeção de models
first()Model|nullPrimeiro registro do filtro
firstOrFail()ModelPrimeiro registro; lança ModelNotFoundException
count()intTotal que casa o filtro
exists() / doesntExist()boolExistência sem carregar registros
sum($col), avg($col), min($col), max($col)mixedAgregações
pluck($value, $key?)CollectionMapa chave => valor para combos

Métodos intermediários (encadeáveis)

MétodoDescrição
where($col, $op, $val)Filtro AND (operador omitido = =)
orWhere(...)Filtro OR
whereIn($col, $values) / whereNotIn(...)IN / NOT IN — aceita array ou outro Builder (subquery)
whereBetween($col, [$a, $b])BETWEEN
whereNull($col) / whereNotNull($col)IS NULL / IS NOT NULL
orderBy($col, $dir = 'asc')ORDER BY
limit($n) / take($n)LIMIT (sinônimos)
offset($n) / skip($n)OFFSET (sinônimos)
select($cols)SELECT col1, col2 — aceita array ou args variádicos
groupBy($col) / having(...)GROUP BY / HAVING
with($relation)Eager load — ver Relacionamentos

Casos comuns

Paginação

// Paginação manual (limit/offset)
$page    = 1;
$perPage = 20;

$itens = Produto::where('ativo', true)
    ->orderBy('nome')
    ->forPage($page, $perPage)
    ->get();

$total = Produto::where('ativo', true)->count();
$pages = (int) ceil($total / $perPage);

// Paginação automática do Laravel (gera meta + links)
$itens = Produto::where('ativo', true)->orderBy('nome')->paginate(20);

Busca multi-coluna

$q = trim(request('q', ''));

$itens = Produto::where('ativo', true)
    ->when($q !== '', function ($query) use ($q) {
        $query->where(function ($w) use ($q) {
            $w->where('nome', 'like', "%{$q}%")
                ->orWhere('codigo', 'like', "%{$q}%")
                ->orWhere('cod_barras', 'like', "%{$q}%");
        });
    })
    ->orderBy('nome')
    ->get();

when($condicao, $callback) aplica a closure só se a condição for verdadeira — evita if espalhado em cima da query.

Agregação

// Total faturado no mês
$total = PedidoVenda::where('mes', date('Y-m'))
    ->where('status', 'pago')
    ->sum('valor_total');

// Top 10 produtos mais vendidos
$top = PedidoVendaItem::selectRaw('produto_id, SUM(quantidade) as total_qtd')
    ->groupBy('produto_id')
    ->orderByDesc('total_qtd')
    ->limit(10)
    ->get();

ORDER BY vindo do usuário — OrderGuard

Valores viram bind PDO automaticamente, mas identificadores não: coluna de ordenação, groupBy e expressões selectRaw/groupByRaw entram na SQL como texto. Quando esse texto vem da UI (clique no cabeçalho da grid, configuração de chart), passe por Mad\Database\OrderGuard — a fonte única de validação usada pela própria grid e pelos charts do framework:

use Mad\Database\OrderGuard;

// Fail-closed: lança exceção e loga [SQL-INJECTION-GUARD] se a cláusula não passar
OrderGuard::validate($ordem, 'ProdutoListagem::query');
$itens = Produto::orderByRaw($ordem)->get();

// Ou checando sem lançar
if (OrderGuard::isSafeOrderBy($ordem)) { /* ... */ }
MétodoDescrição
isSafeOrderBy($clause)Bool — cláusula ORDER BY / GROUP BY só com identificadores
validate($clause, $context)Igual, mas fail-closed: lança exceção e loga o contexto
isSafeExpression($expr)Bool — expressão de SELECT/GROUP BY raw (coluna ou função)
validateExpression($expr, $context)Versão fail-closed da anterior

A allowlist aceita, por termo separado por vírgula: inteiro (posição de GROUP BY), identificador (col), qualificado (tabela.col), alias entre aspas duplas, agregado de um argumento (SUM(col), COUNT(*), MIN(t.c)...) e direção opcional ASC/DESC. Qualquer outra coisa é recusada — é allowlist, não blacklist.

Próximos