f in x
Eloquent ORM Avanzato: Relazioni, Scopes, Accessor e Prevenzione N+1 [Guida Operativa]
> cd .. / HUB_EDITORIALE > Visualizza in Inglese
Analisi dei dati e metriche

Eloquent ORM Avanzato: Relazioni, Scopes, Accessor e Prevenzione N+1 [Guida Operativa]

[2026-06-04] Author: Ing. Calogero Bono

Il tuo database ha 10.000 ordini. Una vista che carica le relazioni degli utenti impiega dieci secondi. Il cliente pensa che il sito sia rotto. Il problema si chiama N+1: succede quando per ogni record esegui una query aggiuntiva. Noi, di Meteora Web, lo vediamo nei progetti che ci arrivano in revisione: codice funzionante ma lento, query invisibili che affondano le performance. In questa guida ti portiamo dritto alle soluzioni: relazioni ben definite, scopes che incapsulano la logica, accessor che trasformano i dati, e le tecniche per uccidere N+1 prima che nasca.

Relazioni Eloquent: Molto Più di belongsTo e hasMany

Le relazioni in Eloquent sono oggetti che puoi concatenare, condizionare, caricare sotto richiesta. Il segreto è usarle come costruzioni query, non come semplici getter.

Definire Relazioni con Chiavi Personalizzate

Di default Eloquent indovina le foreign key. Nei progetti reali le tabelle hanno convenzioni diverse. Specifica sempre chiavi esplicite:

// Modello Order
public function customer(): BelongsTo
{
    return $this->belongsTo(Customer::class, 'customer_id', 'id_customer');
}

public function items(): HasMany
{
    return $this->hasMany(OrderItem::class, 'order_ref', 'reference');
}

Eager Loading Condizionale e Annidato

Il classico with('items') carica tutte le items. Quando hai bisogno solo di un sottoinsieme, usa constrained eager loading:

$orders = Order::with(['items' => function ($query) {
    $query->where('quantity', '>', 0)->orderBy('price', 'desc');
}])->get();

Puoi nidificare fino al livello che serve. Importante: usare with evita N+1 anche con filtri interni.

Relazioni Polimorfiche: Una Tabella per Molte Entità

Quando un modello può appartenere a più modelli (es. commenti su Post e Video), usa le MorphMany. Attenzione agli indici sul campo morph_type e morph_id:

// Modello Post e Video
public function comments(): MorphMany
{
    return $this->morphMany(Comment::class, 'commentable');
}

// Modello Comment
public function commentable(): MorphTo
{
    return $this->morphTo();
}

Cosa fare subito: Controlla i metodi di relazione nei tuoi modelli. Aggiungi foreignKey e localKey espliciti. Usa with per caricare relazioni annidate solo sui rami che servono.

Global Scopes e Local Scopes: Logica Query Riutilizzabile

Gli scopes sono filtri pre-confezionati. Non ripetere where('active', 1) in ogni controller.

Local Scopes: Filtri su Richiesta

// Modello Product
public function scopeActive($query): Builder
{
    return $query->where('active', true)->whereNull('deleted_at');
}

public function scopeByCategory($query, $slug): Builder
{
    return $query->whereHas('category', fn($q) => $q->where('slug', $slug));
}

// Uso
$products = Product::active()->byCategory('abbigliamento')->get();

Incapsula condizioni complesse: join, subquery, raw SQL. I local scopes sono chiamabili come metodi sul builder.

Global Scopes: Filtri Sempre Presenti

Se ogni query su un modello deve avere un filtro (es. multi-tenancy, soft delete), definisci un global scope. Noi li usiamo per isolare clienti in piattaforme multi-azienda:

// App\Scopes\TenantScope
class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('tenant_id', auth()->user()->tenant_id);
    }
}

// Nel modello User
protected static function booted(): void
{
    static::addGlobalScope(new TenantScope());
}

Attenzione: se devi bypassare lo scope in query specifiche, usa withoutGlobalScope().

Cosa fare subito: Rivedi le query ripetute nel codice. Trasformale in local scopes. Valuta se un global scope risolve filtri di contesto (es. lingua, azienda).

Accessor e Mutator: Trasformare Dati Senza Perdere Performance

Gli accessor (getter) e mutator (setter) permettono di manipolare attributi al volo. Ma attenzione: accessor lenti possono moltiplicare le query.

Accessor con Cache Interna

// Modello User
use Illuminate\Database\Eloquent\Casts\Attribute;

protected function fullName(): Attribute
{
    return Attribute::make(
        get: fn ($value, $attributes) => trim($attributes['first_name'] . ' ' . $attributes['last_name'])
    );
}

Gli accessor che leggono relazioni (es. $user->avatar_url) possono innescare N+1 se non gestiti. Soluzione: carica la relazione con with prima di accedere all'accessor. Oppure usa eager load in accessor tramite $this->relationLoaded:

protected function avatarUrl(): Attribute
{
    return Attribute::make(
        get: function ($value) {
            if (!$this->relationLoaded('media')) {
                // Logica di fallback, non caricare automaticamente
                return $this->defaultAvatarUrl();
            }
            return $this->media->first()?->url ?? $this->defaultAvatarUrl();
        }
    );
}

Mutator per Normalizzare Input

protected function email(): Attribute
{
    return Attribute::make(
        set: fn ($value) => strtolower(trim($value))
    );
}

Cosa fare subito: Controlla gli accessor che usano relazioni. Aggiungi una guardia relationLoaded per evitare query implicite. Per le trasformazioni semplici, usa i cast del modello (tipo array, json) invece di accessor.

Prevenzione N+1: Strumenti e Tecniche

L'N+1 è il killer silenzioso delle performance in Laravel. Succede quando in un loop chiami una relazione non caricata. Un cliente e-commerce con 500 prodotti e una relazione category non eager loaded esegue 501 query. Noi lo abbiamo misurato: il tempo di risposta salta da 200ms a 3 secondi.

1. Debug con Query Log (Locale)

Prima di ottimizzare, devi vedere le query. Attiva il log in AppServiceProvider (solo in ambiente locale):

// In boot()
if (app()->environment('local')) {
    DB::listen(function ($query) {
        logger($query->sql, $query->bindings);
    });
}

Alternativa: Laravel Debugbar (pacchetto barryvdh/laravel-debugbar). Mostra numero query, duplicate, eager loading mancante.

2. Usare withCount per Evitare Relazioni Inutili

Se ti serve solo il conteggio (es. numero di ordini per cliente), non caricare la relazione intera. Usa withCount:

$customers = Customer::withCount('orders')->get();
foreach ($customers as $customer) {
    echo $customer->orders_count; // Nessuna query extra
}

3. Lazy Eager Loading Dopo la Query

Non sai subito se ti servirà una relazione? Caricala dopo il primo get() con load():

$orders = Order::all();
if (request()->boolean('include_items')) {
    $orders->load('items.discounts');
}

4. Evitare il Loop di Relazioni con Caching

A volte la relazione è inevitabile in un loop (es. calcolo di un totale). In quel caso, carica la relazione una volta sola e usa remember su collection:

$customers = Customer::with('orders')->get()->remember(60);

Nota: remember sui modelli è un macro che noi abbiamo aggiunto per cache rapida. In alternativa usa Cache::remember con chiave esplicita.

5. Strumento Automatico: Laravel N+1 Detector

In sviluppo, usa il pacchetto beyondcode/laravel-query-detector (o pyaesoneaung/laravel-n-plus-one-detector). Ti avvisa quando stai per commettere N+1. Noi lo attiviamo in tutti i progetti su cui facciamo consulenza.

6. Relazioni Inverse con HasOne

A volte l'N+1 nasce da relazioni inverse non definite. Esempio: un ordine ha un indirizzo, ma vuoi l'ultimo ordine per ogni indirizzo. Crea una relazione latestOrder nel modello Address:

public function latestOrder(): HasOne
{
    return $this->hasOne(Order::class)->latestOfMany();
}

Poi carichi Address::with('latestOrder')->get() – una sola query extra.

Cosa fare adesso: Installa Debugbar. Apri una vista che carica liste. Controlla il numero di query. Ogni query oltre il numero di model + 1 è un N+1 da eliminare. Sostituisci i caricamenti lazy in loop con with. Usa withCount per i conteggi.

In Sintesi — Cosa Fare

  1. Rivedi tutti i metodi di relazione: chiavi esplicite, eager loading condizionale, morph.
  2. Trasforma ogni filtro ripetuto in un local scope. Valuta global scope per contesto globale (tenant, lingua).
  3. Controlla accessor e mutator: non caricare relazioni automaticamente. Usa cast dove possibile.
  4. Attiva il log delle query in locale. Identifica N+1 e risolvili con with, load, withCount.
  5. Integra un detector automatico nei workflow di sviluppo.
  6. Monitora le performance anche in produzione con strumenti come Laravel Telescope.

Noi, di Meteora Web, usiamo questi accorgimenti in ogni progetto Laravel. Risultato: API reattive, admin panel veloci, clienti soddisfatti. Se vuoi che analizziamo le tue query, contattaci. Intanto, apri il tuo modello e inizia da lì.

Sponsored Protocol

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Co-founder di Meteora Web. Ingegnere informatico, sviluppo ecosistemi digitali ad alte prestazioni. AI, automazione, SEO tecnica e infrastrutture web. Scrivo di tecnologia per rendere complesso… semplice.

[ Read Full Dossier ]

Hai bisogno di applicare questa strategia?

Esegui il protocollo di contatto per iniziare un progetto con noi.

> INIZIA_PROGETTO

Sponsored

> MW_JOURNAL

> READ_ALL()