Nella vita di uno sviluppatore arriva spesso il momento in cui bisogna interagire con un database. È qui che entra in scena Eloquent, il mappatore oggetto-relazionale (ORM) di Laravel che permette di interagire con le tabelle del database in modo intuitivo e naturale.

È fondamentale che un professionista riconosca e comprenda i sei tipi di relazione che esamineremo in questo articolo.

Cosa Sono le Relazioni in Eloquent?

Quando si lavora con le tabelle di un database relazionale, le relazioni possono essere definite come connessioni tra tabelle. Queste permettono di organizzare e strutturare i dati, migliorando la leggibilità e la gestione dei dati. In pratica esistono tre tipi di relazioni nei database:

  • uno-a-uno – Un record di una tabella è associato a uno, e uno solo, di un’altra tabella. Ad esempio, una persona e un numero di previdenza sociale.
    • uno-a-molti – Un record è associato a più record di un’altra tabella. Ad esempio, uno scrittore e il suo blog.
  • molti-a-molti – Più record di una tabella sono associati a più record di un’altra tabella. Ad esempio, gli studenti e i corsi a cui sono iscritti.

Con Laravel è semplice interagire e gestire le relazioni tra i database utilizzando la sintassi orientata agli oggetti di Eloquent.

Oltre a queste definizioni, Laravel introduce altre relazioni, ovvero:

  • Has Many Through
  • Relazioni polimorfiche
  • Polimorfica molti-a-molti

Prendiamo, ad esempio, un negozio il cui inventario contiene una varietà di articoli, ognuno dei quali appartiene a una categoria specifica. Dividere il database in più tabelle ha senso dal punto di vista commerciale. Ma questo comporta dei problemi, in quanto non si vuole interrogare ogni singola tabella.

In Laravel possiamo facilmente creare una semplice relazione uno-a-molti. Ad esempio, quando dobbiamo interrogare i prodotti, possiamo farlo utilizzando il modello Product.

Schema di database con tre tabelle e una tabella congiunta che rappresenta una relazione polimorfica
Schema di database con tre tabelle e una tabella congiunta che rappresenta una relazione polimorfica

Relazione Uno-A-Uno

È la prima relazione di base offerta da Laravel e associa due tabelle in modo tale che una riga della prima tabella sia correlata a una sola riga dell’altra tabella.

Per vederla in azione, dobbiamo creare due modelli con la loro migrazione:

php artisan make:model Tenant 
Php artisan make:model Rent

A questo punto abbiamo due modelli: uno è il Tenant (“inquilino”) e l’altro è il Rent (“affitto”).

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    /**
    * Get the rent of a Tenant
    */
    public function rent() 
    {
        return $this->hasOne(Rent::class);
    }
}

Poiché eloquent determina la relazione della foreign key in base al nome del modello parent (Tenant in questo caso), il modello Rent presuppone l’esistenza di una foreign key tenant_id.

Possiamo sovrascriverla aggiungendo un argomento al metodo hasOne:

return $this- >hasOne(Rent::class, "custom_key");

Eloquent presuppone anche che ci sia una corrispondenza tra la chiave esterna definita e la chiave primaria del modello padre (Tenant). Di default, cercherà di far corrispondere tenant_id con la chiave id del record Tenant. Possiamo sovrascrivere questo parametro con un terzo parametro nel metodo hasOne, in modo che corrisponda a un’altra chiave:

return $this->hasOne(Rent::class, "custom_key", "other_key"); 

Ora che abbiamo definito la relazione uno-a-uno tra i modelli, possiamo usarla in questo modo:

$rent = Tenant::find(10)->rent;

Con questa riga di codice otteniamo il Rent (“affitto”) del Tenant (“inquilino”) con l’id 10, se esiste.

Relazione Uno-A-Molti

Come la relazione precedente, anche questa definirà le relazioni tra un modello monoparentale e più modelli figli. È improbabile che il nostro Tenant abbia una sola bolletta dell’affitto perché si tratta di un pagamento ricorrente, quindi avrà più pagamenti.

In questo caso, la relazione precedente presenta dei difetti che possiamo correggere:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    /**
    * Get the rents of a Tenant
    */
    public function rent() 
    {
        return $this->hasMany(Rent::class);
    }
}

Prima di invocare il metodo per ottenere gli affitti, è bene sapere che le relazioni servono a costruire le query, quindi possiamo aggiungere altri vincoli (come le date degli affitti, i pagamenti minimi, ecc.) e concatenarli per ottenere il risultato desiderato:

$rents = Tenant::find(10)->rent()->where('payment', '>', 500)->first();

E come per la relazione precedente, possiamo sovrascrivere le chiavi esterne e locali inserendo altri argomenti:

return $this->hasMany(Rent::class, "foreign_key");
return $this->hasMany(Rent::class, "foreign_key", "local_key");

Ora abbiamo tutti gli affitti di un Tenant, ma cosa facciamo se conosciamo l’affitto e vogliamo capire a chi appartiene? Possiamo utilizzare la proprietà belongsTo:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Rent extends Model
{
    /**
    * Return the tenant for the rent
    */
    public function tenant() 
    {
        return $this->belongsTo(Tenant::class);
    }
}

E ora possiamo trovare facilmente il Tenant:

$tenant = Rent::find(1)->tenant;

Per il metodo belongsTo, possiamo anche sovrascrivere le chiavi esterne e locali come abbiamo fatto in precedenza.

Relazione Has-One-Of-Many

Dato che il nostro modello di Tenant può essere associato a molti modelli di Rent, vogliamo recuperare il modello più recente o più vecchio delle relazioni.

Un modo comodo per farlo è combinare i metodi hasOne e ofMany:

public function latestRent() {
    return $this->hasOne(Rent::class)->latestOfMany();
}

public function oldestRent() {
    return $this->hasOne(Rent::class)->oldestOfMany();
}

Di default, otteniamo i dati in base alla chiave primaria, che è ordinabile, ma possiamo creare i nostri filtri per il metodo ofMany:

return $this->hasOne(Rent::class)->ofMany('price', 'min');

Relazioni HasOneThrough e HasManyThrough

COn i metodi -Through, i nostri modelli dovranno passare attraverso un altro modello per stabilire una relazione con il modello desiderato. Ad esempio, possiamo associare l’Affitto al Locatore, ma l’Affitto deve prima passare attraverso l’Tenantper raggi ungere il Locatore.

Le chiavi delle tabelle necessarie a questo scopo sarebbero le seguenti:

rent
    id - integer
    name - string
    value - double

tenants
    id - integer
    name - string
    rent_id - integer

landlord
    id - integer
    name - string
    tenant_id - integer

Dopo aver visualizzato l’aspetto delle nostre tabelle, possiamo creare i modelli:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Rent extends Model
{
    /**
    * Return the rents' landlord
    */
    public function rentLandlord() 
    {
        return $this->hasOneThrough(Landlord::class, Tenant::class);
    }
}

Il primo argomento del metodo hasOneThrough è il modello a cui accedere e il secondo argomento è il modello da attraversare.

E, proprio come prima, è possibile sovrascrivere le chiavi esterne e locali. Ora che abbiamo due modelli, ne abbiamo due di ciascuno da sovrascrivere in questo ordine:

public function rentLandlord() 
{
    return $this->hasOneThrough(
        Landlord::class,
        Tenant::class,
        "rent_id",    // Foreign key on the tenant table
        "tenant_id",  // Foreign key on the landlord table
        "id",         // Local key on the tenant class
        "id"          // Local key on the tenant table
    );
}

Allo stesso modo, la relazione “Has Many Through” di Laravel Eloquent è utile per accedere ai record di una tabella lontana attraverso una tabella intermedia. Consideriamo un esempio con tre tabelle:

  • paese
  • utenti
  • giochi

Ogni Paese ha molti utenti e ogni utente ha molti giochi. Vogliamo recuperare tutti i giochi appartenenti a un Paese attraverso la tabella Utenti.

Bisognerebbe definire le tabelle in questo modo:

country
    id - integer
    name - string

user
    id - integer
    country_id - integer
    name - string

games
    id - integer
    user_id - integer
    title - string

Ora bisogna definire il modello Eloquent per ogni singola tabella:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
    protected $fillable = ['name'];

    public function users()
    {
        return $this->hasMany(User::class);
    }

    public function games()
    {
        return $this->hasManyThrough(Games::class, User::class);
    }
}
<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $fillable = [article_id, 'name'];

    public function country()
    {
        return $this->belongsTo(Country::class);
    }

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Game extends Model
{
    protected $fillable = ['user_id', 'title'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Ora possiamo invocare il metodo games() del modello Country per ottenere tutti i giochi perché abbiamo stabilito la relazione “Has Many Through” tra Country e Game attraverso il modello User.

<?php

$country = Country::find(159);
            
// Retrieve all games for the country
$games = $country->games;

Relazione Molti-A-Molti

La relazione molti-a-molti è più complicata. Un buon esempio è quello di un dipendente che ha più ruoli. Un ruolo può anche essere assegnato a più dipendenti. Questa è la base della relazione molti-a-molti.

Per questo, dobbiamo avere le tabelle employees, roles e role_employees.

La struttura delle tabelle del nostro database sarà la seguente:

employees
    id - integer
    name - string

roles 
    id - integer
    name - string

role_employees
    user_id - integer
    role_id - integer

Conoscendo la struttura delle tabelle della relazione, possiamo facilmente definire il nostro Modello Employee in modo che appartenga al modello belongsToMany Role.

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Employee extends Model
{
    public function roles() 
    {
        return $this- >belongsToMany(Role::class);
    }
}

Una volta definito questo, possiamo accedere a tutti i ruoli di un dipendente ed anche filtrarli:

$employee = Employee::find(1);
$employee->roles->forEach(function($role) { // });

// OR 

$employee = Employee::find(1)->roles()->orderBy('name')->where('name', 'admin')->get();

Come tutti gli altri metodi, possiamo sovrascrivere le chiavi esterne e locali del metodo belongsToMany.

Per definire la relazione inversa di belongsToMany è sufficiente utilizzare lo stesso metodo, ma con il metodo figlio, e il genitore come argomento.

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    public function employees() 
    {
        return $this->belongsToMany(Employee::class);
    }
}

Utilizzi della Tabella Intermedia

Come abbiamo notato, quando utilizziamo la relazione molti-a-molti, dobbiamo sempre avere una tabella intermedia. In questo caso, utilizziamo la tabella role_employees.

Di default, la nostra tabella pivot conterrà solo gli attributi id. Se vogliamo altri attributi, dobbiamo specificarli in questo modo:

return $this->belongsToMany(Employee::class)->withPivot("active", "created_at");

Se vogliamo ridurre la pivot per i timestamp, possiamo farlo così:

return $this->belongsToMany(Employee::class)->withTimestamps();

Possiamo personalizzare il nome ‘pivot’ con qualsiasi cosa che si adatti meglio alla nostra applicazione:

return $this->belongsToMany(Employee::class)->as('subscription')->withPivot("active", "created_by");

Filtrare i risultati di una query Eloquent è un’operazione indispensabile per tutti gli sviluppatori che vogliono migliorare e ottimizzare le proprie applicazioni Laravel.

Per questo Laravel offre una fantastica funzione pivot che può essere utilizzata per filtrare i dati che vogliamo raccogliere. Così, invece di utilizzare altre funzioni come le transazioni del database per ottenere i nostri dati in frammenti, possiamo filtrarli con metodi come wherePivot, wherePivotIn, wherePivotNotIn, wherePivotBetween, wherePivotNotBetween, wherePivotNull, wherePivotNotNull e possiamo usarli quando definiamo le relazioni tra le tabelle!

return $this->belongsToMany(Employee::class)->wherePivot('promoted', 1);
return $this->belongsToMany(Employee::class)->wherePivotIn('level', [1, 2]);
return $this->belongsToMany(Employee::class)->wherePivotNotIn('level', [2, 3]);
return $this->belongsToMany(Employee::class)->wherePivotBetween('posted_at', ['2023-01-01 00:00:00', '2023-01-02 00:00:00']);
return $this->belongsToMany(Employee::class)->wherePivotNull('expired_at');
return $this->belongsToMany(Employee::class)->wherePivotNotNull('posted_at');

Un’ultima sorprendente caratteristica è la possibilità di ordinare i pivot:

return $this->belongsToMany(Employee::class)
        ->where('promoted', true)
        ->orderByPivot('hired_at', 'desc');

Relazioni Polimorfiche

La parola Polimorfico (o polimorfo) deriva dal greco e significa “molte forme”. Un modello nella nostra applicazione può assumere molte forme, ovvero può avere più di un’associazione. Immaginiamo di costruire un’applicazione con blog, video, sondaggi, ecc. Un utente può creare un commento per ognuno di questi. Pertanto, un modello Comment potrebbe appartenere ai modelli Blog, Video e Sondaggi.

Polimorfica Uno a Uno

Questo tipo di relazione è simile a una relazione standard uno-a-uno. L’unica differenza è che il modello figlio può appartenere a più di un tipo di modello con una singola associazione.

Ad esempio, un modello Tenant e Landlord può condividere una relazione polimorfa con un modello WaterBill.

La struttura della tabella può essere la seguente:

tenants
    id – integer
    name – string

landlords
    id – integer
    name – string

waterbills
    id – integer
    amount – double
    waterbillable_id
    waterbillable_type

Utilizziamo waterbillable_id per l’id del landlord o del tenant, mentre waterbillable_type contiene il nome della classe del modello genitore. La colonna type viene utilizzata da Eloquent per capire quale modello genitore restituire.

La definizione del modello per questa relazione sarà la seguente:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class WaterBill extends Model
{
    public function billable()
    {
        return $this->morphTo();
    }
}

class Tenant extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

class Landlord extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

Una volta che tutto questo è stato messo a punto, possiamo accedere ai dati di entrambi i modelli Landlord e Tenant:

<?php

$tenant = Tenant::find(1)->waterBill;
$landlord = Landlord::find(1)->waterBill;

Una Relazione Polimorfica Uno A Molti

È simile a una normale relazione uno-a-molti; la differenza principale è che il modello figlio può appartenere a più di un tipo di modello, utilizzando una singola associazione.

In un’applicazione come Facebook, gli utenti possono commentare post, video, sondaggi, dirette, ecc. Con un’associazione polimorfa uno a molti, possiamo utilizzare un’unica tabella comments per memorizzare i commenti di tutte le categorie. La struttura delle nostre tabelle sarebbe simile a questa:

posts 
    id – integer
    title – string
    body – text

videos
    id – integer
    title – string
    url – string

polls
    id – integer
    title – string

comments 
    id – integer
    body – text
    commentable_id – integer
    commentable_type – string

Il commentable_id è l’id del record e il commentable_type è il tipo di classe, in modo che eloquent sappia cosa cercare. Per quanto riguarda la struttura del modello, è molto simile a quella polimorfica uno-a-molti:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model 
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Poll extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Live extends Model
{
    public function comments()
    {
        return $this->morphMany(Comments::class, 'commentable');
    }
}

Ora, per recuperare i commenti di un Live, possiamo semplicemente richiamare il metodo find con l’id e ora abbiamo accesso alla classe iterabile dei commenti:

<?php

use App\Models\Live;

$live = Live::find(1);

foreach ($live->comments as $comment) { }

// OR

Live::find(1)->comments()->each(function($comment) { // });
Live::find(1)->comments()->map(function($comment) { // });
Live::find(1)->comments()->filter(function($comment) { // });

// etc.

Se abbiamo un commento e vogliamo scoprire a chi appartiene, possiamo accedere al metodo commentable:

<?php

use App\Models\Comment;

$comment = Comment::find(10);
$commentable = $comment->commentable;

// commentable – type of Post, Video, Poll, Live

Polimorfica Uno A Molti

In molte applicazioni che scalano, vogliamo un modo semplice per interagire con i modelli e tra di essi. Potremmo voler conoscere il primo o l’ultimo post di un utente, il che può essere fatto con una combinazione di metodi morphOne e ofMany:

<?php

public function latestPost()
{
    return $this->morphOne(Post::class, 'postable')->latestOfMany();
}

public function oldestPost()
{
    return $this->morphOne(Post::class, 'postable')->oldestOfMany();
}

I metodi latestOfMany e oldestOfMany recuperano il modello più recente o più vecchio in base alla chiave primaria del modello, ovvero alla condizione che sia ordinabile.

In alcuni casi, non vogliamo ordinare in base all’ID, magari abbiamo cambiato la data di pubblicazione di alcuni post e li vogliamo in quell’ordine, non in base al loro id.

Per questo è possibile passare due parametri al metodo ofMany. Il primo parametro è la chiave per cui vogliamo filtrare e il secondo è il metodo di ordinamento:

<?php

public function latestPublishedPost()
{
    return $this->morphOne(Post::class, "postable")->ofMany("published_at", "max");
}

Tenendo conto di questo, è possibile costruire relazioni più avanzate! Immaginiamo di avere questo scenario. Ci viene chiesto di generare un elenco di tutti i post attuali nell’ordine in cui sono stati pubblicati. Il problema sorge quando abbiamo due post con lo stesso valore di published_at e quando i post sono programmati per essere pubblicati in futuro.

Per farlo, possiamo passare l’ordine in cui vogliamo che i filtri siano applicati al metodo ofMany. In questo modo ordiniamo per published_at e, se sono uguali, ordiniamo per id. In secondo luogo, possiamo applicare una funzione di query al metodo ofMany per escludere tutti i post di cui è prevista la pubblicazione!

<?php

public function currentPosts()
{
    return $this->hasOne(Post::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function ($query) {
        $query->where('published_at', '<', now());
    });
}

Polimorfica Molti A Molti

Il metodo polimorfico molti-a-molti è leggermente più complesso di quello normale. Una situazione frequente è quella in cui i tag si applicano a più risorse nella stessa applicazione. Ad esempio, su TikTok abbiamo tag che possono essere applicati a video, corti, storie, ecc.

Il metodo polimorfico molti-a-molti ci permette di avere un’unica tabella di tag associati ai Video, ai Corti e alle Storie.

La struttura della tabella è semplice:

videos
    id – integer
    description – string

stories 
    id – integer
    description – string

taggables 
    tag_id – integer
    taggable_id – integer
    taggable_type – string

Con le tabelle pronte, possiamo creare il modello e utilizzare il metodo morphToMany. Questo metodo accetta il nome della classe del modello e il “nome della relazione”:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Video extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

Con questo metodo possiamo facilmente definire la relazione inversa. Sappiamo che per ogni modello figlio vogliamo invocare il metodo morphedByMany:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    public function stories()
    {
        return $this->morphedByMany(Story::class, 'taggable');
    }

    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    } 
}

E ora, quando otteniamo un Tag, possiamo recuperare tutti i video e le storie correlate a quel tag!

<?php
use App\Model\Tag;

$tag = Tag::find(10);
$posts = $tag->stories;
$videos = $tag->stories;

Ottimizzare la Velocità di Eloquent

Quando si lavora con l’ORM Eloquent di Laravel, è importante capire come ottimizzare le query sul database e ridurre al minimo il tempo e la memoria necessari per recuperare i dati. Uno dei modi per farlo è implementare la cache nell’applicazione.

Laravel offre un sistema di caching flessibile che supporta diversi backend, come Redis, Memcached e la cache basata su file. Mettendo in cache i risultati delle query di Eloquent, è possibile ridurre il numero di interrogazioni al database, il che permette di rendere un’applicazione più veloce e ne aumenta il valore.

Inoltre, si può utilizzare il query builder di Laravel per creare altre query complesse, ottimizzando ancora di più le prestazioni dell’applicazione.

Riepilogo

In conclusione, le relazioni Eloquent di Laravel permettono agli sviluppatori di lavorare facilmente con dati correlati. Dalle relazioni uno-a-uno a quelle molti-a-molti, Eloquent fornisce una sintassi semplice e intuitiva per definire e interrogare queste relazioni.

La padronanza delle relazioni di Eloquent può migliorare notevolmente il flusso di lavoro di uno sviluppatore e rendere il codice più efficiente e leggibile. Se volete saperne di più su Laravel, Kinsta offre diverse risorse, tra cui un tutorial per iniziare a lavorare con Laravel e un articolo sugli stipendi degli sviluppatori Laravel.

Kinsta offre soluzioni di hosting gestito che rendono la distribuzione e la gestione delle applicazioni Laravel un gioco da ragazzi.

Coman Cosmin

Cosmin Coman è uno scrittore e sviluppatore di tecnologia con oltre 3 anni di esperienza. Oltre a scrivere per Kinsta, ha collaborato alla ricerca presso strutture di fisica nucleare e università. Esperto di tecnologia e integrato nella comunità, propone sempre soluzioni innovative.