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.