Consultes Eloquent

Consultes avançades amb Eloquent: scopes, eager loading, subconsultes, chunking i collections.

Consultes bàsiques#

Eloquent hereta tots els mètodes del Query Builder, cosa que significa que pots utilitzar where(), orderBy(), limit() i tota la resta directament des del model. La diferència és que Eloquent retorna instàncies del model en lloc d'objectes genèrics:

// Tots els articles publicats, ordenats per data
$articles = Article::where('published', true)
    ->orderBy('published_at', 'desc')
    ->get();
 
// Primer article de l'autor
$latest = Article::where('user_id', $user->id)
    ->latest()
    ->first();
 
// Comptar articles publicats
$count = Article::where('published', true)->count();
 
// Obtenir una col·lecció de valors d'una columna
$titles = Article::where('published', true)->pluck('title');
 
// Pluck clau-valor
$articles = Article::pluck('title', 'id');
// [1 => 'Primer article', 2 => 'Segon article', ...]

El mètode get() retorna una Collection d'Eloquent, que estén les col·leccions de Laravel amb mètodes específics per a models. El mètode first() retorna una instància del model o null si no troba cap resultat.

Consultes condicionals#

El mètode when() permet afegir condicions a la consulta de manera condicional, cosa que és especialment útil quan construeixes consultes basades en paràmetres de la petició:

$articles = Article::query()
    ->when($request->search, function ($query, $search) {
        $query->where('title', 'like', "%{$search}%");
    })
    ->when($request->category, function ($query, $category) {
        $query->where('category_id', $category);
    })
    ->when($request->sort === 'oldest', function ($query) {
        $query->oldest();
    }, function ($query) {
        $query->latest(); // Valor per defecte si la condició és falsa
    })
    ->paginate(15);

Sense when(), hauries d'escriure condicionals if separats que trenquen la fluïdesa de la cadena de mètodes. El mètode when() només executa el closure si el primer paràmetre és truthy.

Scopes#

Els scopes permeten encapsular condicions de consulta reutilitzables dins del model. En lloc de repetir les mateixes condicions where() per tot el codi, les defineixes una vegada al model i les reutilitzes amb un mètode encadenable.

Scopes locals#

Un scope local és un mètode al model prefixat amb scope. Laravel elimina automàticament el prefix quan el crides:

class Article extends Model
{
    public function scopePublished($query)
    {
        return $query->where('published', true)
                     ->whereNotNull('published_at');
    }
 
    public function scopeDraft($query)
    {
        return $query->where('published', false);
    }
 
    public function scopeByAuthor($query, User $user)
    {
        return $query->where('user_id', $user->id);
    }
 
    public function scopeRecent($query, int $days = 7)
    {
        return $query->where('created_at', '>=', now()->subDays($days));
    }
 
    public function scopePopular($query)
    {
        return $query->orderBy('views', 'desc');
    }
}

Ara pots combinar scopes de manera fluida:

// Articles publicats de l'últim mes
$articles = Article::published()->recent(30)->get();
 
// Articles publicats d'un autor, ordenats per popularitat
$articles = Article::published()
    ->byAuthor($user)
    ->popular()
    ->limit(10)
    ->get();
 
// Esborranys recents
$drafts = Article::draft()->recent()->get();

Scopes globals#

Un scope global s'aplica automàticament a totes les consultes d'un model. Això és útil quan sempre vols filtrar registres per una condició concreta:

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
 
class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('active', true);
    }
}

Per aplicar-lo al model, utilitza l'atribut ScopedBy o registra'l al mètode booted():

use Illuminate\Database\Eloquent\Attributes\ScopedBy;
 
#[ScopedBy(ActiveScope::class)]
class User extends Model
{
    // Totes les consultes inclouran WHERE active = true
}

Si necessites fer una consulta sense el scope global, utilitza withoutGlobalScope():

// Ignorar el scope global
$allUsers = User::withoutGlobalScope(ActiveScope::class)->get();
 
// Ignorar tots els scopes globals
$allUsers = User::withoutGlobalScopes()->get();

Eager Loading#

L'eager loading és una de les tècniques més importants per al rendiment amb Eloquent. Quan accedeixes a una relació d'un model, Eloquent la carrega "lazy" per defecte: fa la consulta a la base de dades en el moment en què accedeixes a la propietat. Això crea el problema N+1:

// Problema N+1: 1 consulta per obtenir articles + 1 consulta per CADA autor
$articles = Article::all(); // SELECT * FROM articles (1 consulta)
 
foreach ($articles as $article) {
    echo $article->author->name;
    // SELECT * FROM users WHERE id = ? (N consultes!)
}

Si tens 100 articles, això genera 101 consultes a la base de dades. L'eager loading resol aquest problema carregant totes les relacions en una o dues consultes:

// Eager loading: 2 consultes en total
$articles = Article::with('author')->get();
// SELECT * FROM articles
// SELECT * FROM users WHERE id IN (1, 2, 3, ...)
 
foreach ($articles as $article) {
    echo $article->author->name; // Ja carregat, sense consulta addicional
}

Carregar múltiples relacions#

Pots carregar diverses relacions al mateix temps, incloent relacions niuades:

// Múltiples relacions
$articles = Article::with(['author', 'comments', 'tags'])->get();
 
// Relacions niuades (l'autor dels comentaris)
$articles = Article::with('comments.author')->get();
 
// Seleccionar columnes específiques de la relació
$articles = Article::with('author:id,name,email')->get();

Eager loading amb restriccions#

Pots afegir condicions a les relacions carregades:

$articles = Article::with(['comments' => function ($query) {
    $query->where('approved', true)
          ->latest()
          ->limit(5);
}])->get();
 
// Amb múltiples relacions i restriccions
$articles = Article::with([
    'author',
    'comments' => fn ($query) => $query->where('approved', true),
    'tags' => fn ($query) => $query->orderBy('name'),
])->get();

Lazy eager loading#

Si ja tens una col·lecció de models i necessites carregar una relació després, utilitza load():

$articles = Article::all();
 
// Decisió condicional de carregar relacions
if ($includeComments) {
    $articles->load('comments');
}
 
// Carregar si no s'ha carregat encara
$articles->loadMissing('comments');

Prevenir lazy loading#

Per assegurar-te que mai oblides l'eager loading, pots desactivar el lazy loading durant el desenvolupament. Això llançarà una excepció si intentes accedir a una relació no carregada:

// A AppServiceProvider::boot()
use Illuminate\Database\Eloquent\Model;
 
Model::preventLazyLoading(! app()->isProduction());

Això ajuda a detectar problemes N+1 durant el desenvolupament sense afectar la producció.

Subconsultes#

Les subconsultes permeten afegir dades calculades a les teves consultes sense carregar relacions completes. Són molt eficients perquè tot es resol en una sola consulta SQL:

// Afegir l'última data de login com a subconsulta
$users = User::addSelect([
    'last_login_at' => Login::select('created_at')
        ->whereColumn('user_id', 'users.id')
        ->latest()
        ->limit(1),
])->get();
 
// Ordenar per una subconsulta
$users = User::orderByDesc(
    Article::select('published_at')
        ->whereColumn('user_id', 'users.id')
        ->latest()
        ->limit(1)
)->get();

Pots combinar subconsultes amb withCount() i withSum() per afegir agregats de relacions:

// Nombre de relacions
$users = User::withCount('articles')->get();
// Accés: $user->articles_count
 
// Nombre amb condicions
$users = User::withCount([
    'articles',
    'articles as published_count' => fn ($query) => $query->where('published', true),
])->get();
 
// Sumes, mitjanes i altres agregats
$users = User::withSum('orders', 'total')->get();
// Accés: $user->orders_sum_total
 
$users = User::withAvg('reviews', 'rating')->get();
// Accés: $user->reviews_avg_rating

Chunking i lazy collections#

Quan necessites processar milers o milions de registres, carregar-los tots a memòria d'un cop pot causar problemes. Eloquent proporciona diversos mètodes per processar registres en lots:

Chunk#

El mètode chunk() carrega els registres en grups de la mida especificada:

Article::where('published', true)
    ->orderBy('id')
    ->chunk(200, function ($articles) {
        foreach ($articles as $article) {
            // Processar cada article
        }
    });

Si necessites modificar registres durant el chunking, utilitza chunkById() per evitar problemes amb l'offset:

Article::where('published', false)
    ->chunkById(200, function ($articles) {
        $articles->each->update(['archived' => true]);
    });

Lazy collections#

Les lazy collections utilitzen generadors PHP per processar un registre a la vegada, mantenint l'ús de memòria extremament baix:

Article::where('published', true)
    ->lazy()
    ->each(function ($article) {
        // Processar un article a la vegada
    });
 
// Amb condicions
Article::lazy()->filter(function ($article) {
    return $article->views > 1000;
})->each(function ($article) {
    // Processar articles populars
});

La diferència amb chunk() és que lazy() retorna una col·lecció que pots encadenar amb filter(), map() i altres mètodes de col·lecció. chunk() executa un callback per cada grup.

Col·leccions Eloquent#

Quan Eloquent retorna múltiples resultats, els empaqueta en una instància de Illuminate\Database\Eloquent\Collection, que estén la classe Collection base de Laravel amb mètodes específics per a models:

$articles = Article::where('published', true)->get();
 
// Mètodes de col·lecció estàndard
$titles = $articles->pluck('title');
$grouped = $articles->groupBy('category_id');
$filtered = $articles->where('views', '>', 100);
 
// Mètodes específics d'Eloquent
$articles->load('author');           // Lazy eager load
$articles->loadMissing('comments');  // Carregar si no carregat
 
// Trobar dins la col·lecció (sense consulta)
$article = $articles->find(1);
 
// Diferència amb una altra col·lecció per ID
$new = $articles->diff($oldArticles);
 
// Intersecar amb una altra col·lecció per ID
$common = $articles->intersect($otherArticles);

Les col·leccions són immutables: cada mètode retorna una nova col·lecció sense modificar l'original. Això fa que siguin segures de manipular sense efectes secundaris inesperats.