CONTATTACI

Guide per aspiranti programmatori

Lezione 21 / 30

Eloquent e le relazioni tra tabelle in Laravel

Nella introduzione ai database relazionali abbiamo visto che è possibile (e consigliato) creare relazioni tra varie tabelle nel momento in cui esiste una relazione tra le entità che sono rappresentate nelle tabelle. Una tabella movies potrà collegarsi sia alla tabella actors che alla tabella directors.

Esistono diversi tipi di relazioni possibili in un database, ognuna indicata per rappresentare una determinata situazione o modellazione (Clint Eastwood è sia attore che regista … sarà presente in entrambe le tabelle? In entrambe indicheremo giorno e luogo di nascita e in entrambe aggiorneremo eventuali informazioni più recenti?).

Con Laravel e Eloquent è possibile gestire tali relazioni in modo semplice, sia per quanto riguarda la creazione delle tabelle e delle relative foreign key, sia per quanto riguarda la gestione tramite i Model.

Convenzioni e utilizzo base in Laravel

Supponiamo di avere creato nella nostra applicazione due modelli (Author e Book, per ora vuoti) e le relative migrazioni per indicare le colonne delle rispettive tabelle authors e books.

// nel metodo up() della migrazione che ha aggiunto la tabella "authors"
Schema::create('authors', function (Blueprint $table) {
  $table->id();
  $table->timestamps();
  $table->text('first_name');
  $table->text('last_name');
});

// nel metodo up() della migrazione che ha aggiunto la tabella "books"
Schema::create('books', function (Blueprint $table) {
  $table->id();
  $table->timestamps();
  $table->string('title')
});

Per creare una relazione tra le due tabelle (libro B dell’autore A) e fare in modo che Eloquent possa automaticamente riconoscere questa relazione, è sufficiente seguire alcune convenzioni nella creazione della foreign key che associa autori a libri.

// in Schema::create('books' ..

  $table->foreignId('author_id');
  $table->foreign('author_id')->references('id')->on('authors');

La colonna con la foreign key ha come nome il nome del modello al singolare e minuscolo (author nel nostro caso) seguito dal suffisso _id.

Una volta creata la colonna per la foreign key, per completare la connessione tra i due model serve solo indicare nel model App\Model\Book la relazione che lo lega al model App\Model\Author. Nel nostro caso, un libro appartiene a un unico singolo autore.

class Book extends Model
{
  public function author()
  {
    return $this->belongsTo(Author::class);
  }
}

In questo modo è possibile scrivere una rotta come la seguente nella quale sarà possibile, a partire dal libro recuperato dal database, estrarre le informazioni sul suo autore, grazie alla property dinamica aggiunta dal Eloquent.

Route::get('/books/{book}', function (Book $book){
  // property dinamica author aggiunta da Eloquent
  $author = $book->author;
 
  // posso accedere a tutte le property di Author
  return "$book->title - $author->first_name $author->last_name";
});

Come già visto per i singoli model, è sufficiente seguire le convenzioni previste da Eloquent per nomi di tabelle e colonne e indicare nel model il tipo di relazione che lega le due entità per avere la possibilità di interagire con strutture dati collegate tra loro.

Laravel ed Eloquent si occuperanno di costruire ed eseguire le effettive query necessarie a recuperare l’autore del libro che stiamo chiedendo di visualizzare e restituiranno un’ istanza del model collegato.

Esistono diversi tipi di relazione che possono collegare una tabella a un’altra tramite foreign key. Le due più importanti e frequenti sono One to Many e Many to Many.

Alla base di ogni relazione che è possibile utilizzare con Eloquent vi sono due condizioni particolari:

  • l’aver rispettato le convenzioni sui nomi di tabelle e colonne, in particolare la necessità di usare <altra_tabella>_id per le colonne foreign key
  • l’aver dichiarato i metodi “di relazione” nelle classi del model

Relazione One to Many in Laravel

Esiste una relazione one-to-many tra due tabelle A e B quando una riga della tabella A può essere legata con molte righe della tabella B, ma ogni riga della tabella B è legata a solo una riga della tabella A.

È il caso, questo, della relazione tra Autor (A) e Book (B) che abbiamo parzialmente introdotto poco fa. Nell’ultimo esempio, abbiamo esplicitato che un’ istanza/riga di Book è collegata a una sola istanza/riga di Author tramite il metodo belongsTo.

Per poter completare una relazione one-to-many tra due modelli Eloquent è, infatti, necessario che in entrambi i model sia definito il tipo di relazione.

Nel nostro caso, quindi, possiamo indicare che ad una singola istanza di Author possono corrispondere più istanze di Book nel seguente modo:

class Author extends Model
{
  public function books()
  {
    return $this->hasMany(Book::class);
  }
}

Sarà, quindi, possibile accedere, partendo da un autore recuperato dal database, all’elenco dei suoi libri nei modi previsti dalle varie convenzioni di Eloquent

Route::get('/author/{author}', function (Author $author){
  return view('author.show', [
    'author' => $author
  ]);
});

{{-- resources/views/author/show.blade.php --}}
<div>
  {{ $author->first_name }} {{ $author->last_name }}
  <ul>
    @foreach($author->books as $book)
      <li>{{ $book->title }}</li>
    @endforeach
  </ul>
</div>

Relazioni e query efficienti (lazy-load vs eager) in Laravel

Laravel ed Eloquent permettono di semplificare alcuni passaggi dello sviluppo web, ma è comunque necessario sapere come il framework si comporta in determinate situazioni.

Prendiamo, ad esempio, il caso in cui, con la relazione one-to-many impostata in precedenza, volessimo realizzare una pagina in cui sono elencati tutti i libri con il rispettivo autore.

use App\Models\Book;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;

Route::get('/books', function (){
  // stampa nel file di log le query effettivamente fatte da Eloquent
  // nel creare la riposta per questa richiesta, utile per il debug
  DB::listen(function ($query) {
    logger($query->sql);
  });

  return view('book.index', [
    'books' => Book::all()
  ]);
});

<div class="container">
  <ul>
    @foreach ($posts as $post)
      <li>
        <a href="/post/{{$post->id}}">{{ $post->title }}</a> by <a
          href="/author/{{$post->author->id}}">{{$post->author->first_name}}</a>
      </li>
    @endforeach
  </ul>
</div>

Leggendo il file storage/logs/laravel.log dopo aver aperto la rotta, scopriremo che, per ognuno dei libri salvati sul nostro database, Laravel avrà effettuato una query per estrarre le informazioni del relativo autore.

local.DEBUG: select * from "posts"
local.DEBUG: select * from "authors" where "authors"."id" = ? limit 1
local.DEBUG: select * from "authors" where "authors"."id" = ? limit 1
local.DEBUG: select * from "authors" where "authors"."id" = ? limit 1
...

Nel loro funzionamento normale, infatti, Laravel ed Eloquent applicheranno di default una modalità di recupero delle informazioni definita lazy-loading. Ovvero, Laravel non eseguirà le query legate alle relazioni a meno che non servano.

Il che, di solito, è un bene, perché evita che vengano eseguite query inutili nel caso in cui si volesse avere, ad esempio, solo la lista di tutti i libri presenti nel database per utilizzare i rispettivi titoli.

Nel caso specifico della rotta e della view create, stiamo accedendo anche alle informazioni della tabella authors collegata, quindi Laravel/Eloquent dovrà effettuare le query necessarie nel momento in cui serve il dato (cioè quanto viene chiesto nome e cognome dell’autore da inserire nella view). Ciò scatena l’esecuzione di tutte le altre query necessarie a recuperare il dato mancante.

È opportuno essere molto attenti, nel momento in cui si usa Eloquent e le relazioni tra tabelle, a cosa faremo con l’info che stiamo chiedendo, poiché potrebbero esserci modi più efficienti di recuperare quelle informazioni.

Nel caso specifico dell’esempio, potrebbe essere più efficiente seguire un comportamento diverso da quello lazy-loading.

Sapendo già che per quella rotta e quella view useremo sia le informazioni sul libro che quelle sull’autore collegato, è possibile usare la modalità eager per recuperare subito dal database anche la lista degli autori.

La modalità eager va, quindi, espressamente richiesta, in particolare indicando quale tabella collegata andare a recuperare subito.

Route::get('/books', function (){
  DB::listen(function ($query) {
    logger($query->sql);
  });

  return view('book.index', [
    // indica al query builder che useremo anche author, quindi può usare la modalità eager
    'books' => Book::with('author')->get()
  ]);
});

Con questa modifica, il builder di query di Laraver riceverà l’indicazione di non usare il lazy-loading per la relazione indicata. Nel log vedremo, quindi, eseguire solo due query a fronte delle stessa richiesta.

local.DEBUG: select * from "posts"
local.DEBUG: select * from "authors" where "authors"."id" in (1, 2, 3, 4, 5, 6, [...])

Relazione Many to Many in Laravel

Altro tipo di relazione particolarmente rilevante durante lo sviluppo software è quella many-to-many, poiché possibile in molte delle entità che il nostro software potrebbe dover rappresentare e manipolare.

Nel modello autore-libro considerato prima, abbiamo dato per assunto che un libro potesse essere scritto da un solo autore. Nella realtà è, invece, possibile che uno stesso libro sia scritto da più autori. In questo caso, abbiamo una relazione many-to-many, poiché una riga della tabella A (Author) può essere legata con molte righe della tabella B (Book) e al tempo stesso una riga della tabella B può essere legata a molte righe della tabella A.

Modelli relazionali

Modelli relazionali

Sono molte le entità che possono essere collegate con una relazione many-to-many: Ordine-Prodotto, Utente-Ruolo, Post-Category, …

Le relazioni many-to-many sono tipicamente implementate in un RDBMS sfruttando una terza tabella, indicata come “tabella associativa” (o join table o anche cross-reference-table). La tabella associativa permette di separare la relazione many-to-many in due distinte relazioni one-to-many

Tabella associativa

Tabella associativa

L’implementazione di una relazione many-to-many in Eloquent richiede la presenza di tre tabelle. Continuando nell’esempio di autori e libri, necessiteremmo, quindi, delle tabelle books, authors e della tabella associativa author_book.

Per convenzione, Eloquent richiede che:

  • il nome della tabella associativa sia formato dal nome dei rispettivi modelli al singolare, uniti da un _ e in ordine alfabetico
  • nella tabella associativa siano presenti le colonne per le due foreign key che puntano a ciascuna tabella, nel formato già visto per la relazione one-to-many (quindi author_id e book_id)

A differenza di quanto fatto nel precedente caso per l’associazione one-to-many tra autore e libro, non saranno presenti foreign key nelle due tabelle books e authors, poiché non vi è una relazione diretta tra le due.

Potremmo, quindi, riassumere visivamente una struttura base come la seguente:

author:
  – id = int
  – name = string

book:
  – id = int
  – title = string

author_book:
  – id = int
  – author_id = foreign_key(author.id)
  – book_id = foreign_key(book.id)

Una volta create tabelle e colonne con i dovuti nomi tramite le migrazioni, sarà necessario indicare le relazioni Eloquent nei relativi model. In questo caso, si tratterà di due relazioni belongsToMany per entrambi i model.

class Author extends Model
{
  public function books()
  {
    return $this->belongsToMany(Books::class);
  }
}

class Book extends Model
{
  public function authors()
  {
    return $this->belongsToMany(Author::class);
  }
}

Sarà, quindi, possibile utilizzare le property dinamiche messe a disposizione da Laravel per accedere all’uno e all’altro model, sulle quali poi applicare ogni altra possibilità messa a disposizione dal sistema di query di Laravel.

use App\Models\Author;

$user = Author::find(1);

foreach ($author->books as $role) {
  //
}

use App\Models\Book;

$book = Book::find(100);

foreach ($book->authors as $author) {
  //
}

Contattaci senza impegno per informazioni sul corso

Pagamento rateale

Valore della rata: A PARTIRE DA 115 €/mese.

Esempio di finanziamento 

Importo finanziato: € 2440 in 24 rate da € 115 – TAN fisso 9,55% TAEG 12,57% – importo totale del credito € 2841.

Il costo totale del credito comprende: interessi calcolati al TAN indicato, oneri fiscali (imposta di bollo sul contratto 16,00 euro*) addebitati sulla prima rata, costo mensile di gestione pratica € 3,90, spesa di istruttoria € 0,00, spesa per invio rendicontazione periodica cartacea € 0,98 (o spesa per invio rendicontazione periodica cartacea € 0,00), imposta di bollo su rendicontazione periodica € 0,00. Modalità di rimborso obbligatoria: addebito diretto su c/c. La scadenza delle rate è determinata dal giorno della liquidazione del contratto; la data di scadenza delle rate è prevista il giorno 15 del mese. L’importo di ciascuna rata comprende una quota di capitale crescente e interessi decrescente secondo un piano di ammortamento “alla francese”. Offerta valida dal 01/01/2024 al 31/12/2024.

Messaggio pubblicitario con finalità promozionale. Per le informazioni precontrattuali richiedere sul punto vendita il documento “Informazioni europee di base sul credito ai consumatori” (SECCI) e copia del testo contrattuale. Salvo approvazione di Sella Personal Credit S.p.A. Aulab S.r.l. opera quale intermediario del credito NON in esclusiva.

*In fase di richiesta del finanziamento verrà proposta la facoltà di selezionare, in alternativa all’imposta di bollo sul contratto di 16,00 euro, l’imposta sostitutiva, pari allo 0,25% dell’importo finanziato.

Pagamento rateale

Valore della rata: A PARTIRE DA 210 €/mese.

Esempio di finanziamento  

Importo finanziato: € 4500 in 24 rate da € 210,03 – TAN fisso 9,68% TAEG 11,97% – importo totale del credito € 5146,55.

Il costo totale del credito comprende: interessi calcolati al TAN indicato, oneri fiscali (imposta di bollo sul contratto 16,00 euro*) addebitati sulla prima rata, costo mensile di gestione pratica € 3,90, spesa di istruttoria € 0,00, spesa per invio rendicontazione periodica cartacea € 0,98 (o spesa per invio rendicontazione periodica cartacea € 0,00), imposta di bollo su rendicontazione periodica € 0,00. Modalità di rimborso obbligatoria: addebito diretto su c/c. La scadenza delle rate è determinata dal giorno della liquidazione del contratto; la data di scadenza delle rate è prevista il giorno 15 del mese. L’importo di ciascuna rata comprende una quota di capitale crescente e interessi decrescente secondo un piano di ammortamento “alla francese”. Offerta valida dal 01/01/2024 al 31/12/2024.

Messaggio pubblicitario con finalità promozionale. Per le informazioni precontrattuali richiedere sul punto vendita il documento “Informazioni europee di base sul credito ai consumatori” (SECCI) e copia del testo contrattuale. Salvo approvazione di Sella Personal Credit S.p.A. Aulab S.r.l. opera quale intermediario del credito NON in esclusiva.

* In fase di richiesta del finanziamento verrà proposta la facoltà di selezionare, in alternativa all’imposta di bollo sul contratto di 16,00 euro, l’imposta sostitutiva, pari allo 0,25% dell’importo finanziato.

Contattaci senza impegno per informazioni sul corso

Scopriamo insieme se i nostri corsi fanno per te. Compila il form e aspetta la chiamata di uno dei nostri consulenti.