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_ratingChunking 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.
Eloquent genera SQL per sota. Si vols entendre millor les consultes que es generen, prova el Playground de MySQL o l'agent d'IA per a SQL.