
Tabelle denormalizzate con Eloquent
alberto • September 27, 2020
tutorialEloquent è fantastico, questo è indiscutibile.
Spesso però non abbiamo la possibilità di modificare il database una volta che prendiamo in mano un progetto legacy.
Nelle ultime settimane ho dovuto lavorare con una tabella denormalizzata, con valori ripetuti all'interno delle colonne senza di fatto la possibilità di creare relazioni. Questo, oltre a diminuire il piacere di lavorare, obbliga spesso a usare query raw per la necessità di introdurre spesso distinct
o group by
.
Immaginiamo di avere a disposizone una tabella con questi dati:
-----------------------------------------------------
| id | name | city | type | value |
-----------------------------------------------------
| 1 | Alberto | Varese | Cliente | 100 |
| 1 | Alessandra | Varese | Fornitore | 50 |
| 1 | Marco | Milano | Cliente | 130 |
| 1 | Ambrogio | Roma | Cliente | 654 |
| 1 | Tommaso | Lecce | Fornitore | 140 |
| 1 | Luca | Padova | Cliente | 543 |
-----------------------------------------------------
In questo caso, se volessimo offrire all'amministratore un pannello per monitorare le performance aziendali con qualche filtro, dovremmo agire più o meno in questo modo:
$cities = Contact::select('city')->distinct()->get(); //per popolare una <select>
$types = Contact::select('type')->distinct()->get(); //per popolare una <select>
$contacts = Contact::all();
Una soluzione sicuramente funzionante, ma poco scalabile e poco object-oriented. Inoltre un eventuale dump delle variabili $cities o $type darebbe in output un classname poco chiaro, considerando che sono istanze della classe Contact
.
Una soluzione interessante
E se provassimo a "forzare" la mano e a creare due nuovi modelli City
e Type
? Una possibilità potrebbe essere quella di sfruttare i Global Scope di Eloquent. Questi sono una particolare tipologia di scope che vengono sempre invocati ad ogni select invocata sul modello.
class City extends Model
{
public $table = 'contacts';
protected static function booted()
{
static::addGlobalScope('distinct_city', function (Builder $builder) {
$builder->select('city')->distinct();
});
}
}
class Type extends Model
{
public $table = 'contacts';
protected static function booted()
{
static::addGlobalScope('distinct_type', function (Builder $builder) {
$builder->select('type')->distinct();
});
}
}
Questi global scope mi hanno permesso di avere due modelli virtuali, che si appoggiano comunque alla tabella principale contacts
ma che utilizzano sempre la clausola distinct per ogni select. Entrambi i modelli non hanno un id e l'unico attributo che presentano è rispettivamente city
e type
.
Grazie a questi modelli "particolari", possiamo ora sfruttare le relazioni all'interno del nostro modello principale Contact.
class Contact extends Model
{
public function city()
{
return $this->belongsTo(City::class, 'city', 'city');
}
public function type()
{
return $this->belongsTo(Type::class, 'type', 'type');
}
}
Possiamo quindi modificare in questo modo il controller:
$cities = City::all();
$types = Type::all();
$contacts = Contact::all()
Oltre ad aver incrementato la leggibilità e l'eleganza del codice, abbiamo migliorato la object-orientation del codice, utilizando classi più parlanti e più aderenti al modello di business nel quale stiamo sviluppando.
Una soluzione interessante
L'ultima parte da implementare è opzionale da un punto di vista funzionale ma evita eventuali errori, soprattutto se lavoriamo in un team più o meno corposo.
I nostri modelli virtuali, essendo appunto modelli veri e propri, presentano una pletora di funzionalità che in questo caso non devono poter essere attivate. In particolare tutte le operazioni di scrittura nel database rischiano di fatto di compromettere la struttura del dato sottostate. Per questa ragione sarebbe una buona prassi modificare questi modelli per evitare che qualcuno, che non abbia una conoscenza completa, possa commettere un errore.
La soluzione che ho scelto è stata quella di utilizzare questo pacchetto che permette appunto, tramite un comodo trait, di rendere inutilizzabili tutti i metodi di scrittura. Ad ogni invocazione di un metodo come save
o delete
, verrà lanciata una eccezione, ricordandoci che il modello è in modalità read-only.
Tiriamo le somme
Una delle qualità che più apprezzo di Eloquent è la sua flessibilità. Nonostante offra una serie di funzionalità implementabili in modo triviale, il framework garantisce la possibilità di personalizzarlo, anche in maniera intensiva.
Ê importante quindi conoscerlo bene per poterlo adattare al massimo alle le nostre esigenze, che spesso saranno particolari.