Usare le Migrations di Laravel per versionare lo schema del database

Le migrazioni di Laravel consentono di versionare lo schema del database dell’applicazione e di legarlo in modo indissolubile al resto del software. Detto in maniera più semplice, le migrations sono delle classi di Laravel che, attraverso la Facade Schema, ci permettono di creare e modificare lo schema del database, in qualsiasi ambiente andremo ad installare l’applicazione.

In questo capitolo impareremo ad usarle. Cominciamo a localizzarle, aprendo la cartella database/migrations

Come possiamo vedere, in questa cartella ci sono alcuni files con estensione PHP. I files hanno un prefisso che indica la data in cui sono stati creati, seguito da un nome che ci dice l’azione della migration. Le 4 migrations che vediamo sono incluse di default in ogni progetto.

Ogni migration ha due metodi: up() e down(). Il primo viene eseguito per aggiungere una modifica allo schema, il secondo invece ha il compito di riportare lo schema alla situazione precedente.

In pratica, i metodi up() delle migrations vengono invocati quando si lancia questo comando

php artisan migrate

Al contrario, i metodi down() delle migrations vengono invocati quando si lancia questo comando

php artisan migrate:rollback

Detto questo, prima di buttarci a capofitto nella scrittura delle migrations facciamo quello che dovrebbe fare ogni buon programmatore: progettare il database.

Database Laravel

Il nostro obiettivo è versionare questo database composto da cinque entità: Produttore, Modello, Macchina, Utente e Patente. La tabella car_user ci servirà per definire la relazione molti a molti tra macchine e utenti, considerando che un utente può possedere più macchine e la stessa macchina può essere cointestata a più persone. Inoltre dobbiamo conservare uno storico per risalire ad ogni passaggio di proprietà

Cominciamo modificando la migrations degli utenti, eliminando il campo name e aggiungendo i campi stringa first_name e last_name. Il metodo up di questa migration si dovrà presentare così

public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->string('first_name');
        $table->string('last_name');
        $table->string('email')->unique();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password');
        $table->rememberToken();
        $table->timestamps();
    });
}

Come vedete, stiamo usando il metodo create() della Facade Schema. Il primo parametro è il nome della tabella, mentre il secondo è una closure che gestisce la creazione dei campi. L’istruzione $table->timestamps() andrà a creare due campi di tipo data, created_at e updated_at, che Laravel utilizza per tenere traccia della creazione e dell’aggiornamento di ogni singolo record.

Adesso creiamo la migration dei produttori, come sempre utilizzando artisan

php artisan make:migration create_producers_table

Apriamo il file appena creato e implementiamo il metodo up() per aggiungere i campi necessari alla tabella.

public function up()
{
    Schema::create('producers', callback: function (Blueprint $table) {
        $table->id();
        $table->string('name', 255);
        $table->timestamps();
    });
}

Ripetiamo la stessa operazione per creare la migration della tabella car_models, ma in questo caso nel metodo up dobbiamo impostare anche la relazione con la tabella producers

php artisan make:migration create_car_models_table
public function up()
{
    Schema::create('car_models', callback: function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('producer_id');
        $table->foreign('producer_id')->references('id')->on('producers');
        $table->string('name', 255);
        $table->decimal('price', 9, 3);
        $table->timestamps();
    });
}

Adesso creiamo la migration della tabella driving_licenses, dove impostiamo la relazione con la tabella users

php artisan make:migration create_driving_licenses_table
public function up()
{
    Schema::create('driving_licenses', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('user_id');
        $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        $table->string('number', 255);
        $table->timestamp('valided_until_at');
        $table->timestamps();
    });
}

Ora occupiamoci della migration per la tabella cars, dove ci sarà una relazione con la tabella car_models. E qui dobbiamo fare un appunto: le migrations devono girare in ordine sequenziale, a partire dal timestamp impostato come prefisso del file. Quindi dobbiamo fare attenzione quando impostiamo le relazioni, perché quando Laravel deve creare un vincolo relazionale, se non trova la tabella da relazionare, va in errore.

Detto questo, possiamo generare la migration

php artisan make:migration create_cars_table

Apriamo il nuovo file e inseriamo i campi come segue

public function up()
{
    Schema::create('cars', callback: function (Blueprint $table) {
        $table->id();
        $table->string('license_plate', 15);
        $table->unsignedBigInteger('car_model_id');
        $table->foreign('car_model_id')->references('id')->on('car_models')->onDelete('cascade');
        $table->timestamp('registered_at')->nullable();
        $table->timestamps();
    });
}

L’ultimo step è la creazione della pivot che ci serve per gestire la relazione molti a molti tra utenti e macchine. Qui utilizzeremo un ulteriore timestamp, per indicare la fine della decorrenza.

La pivot deve chiamarsi come la combinazione dei nomi delle due tabelle che la compongono (in questo caso al singolare), separate da un underscore.

php artisan make:migration create_car_user_table

E di seguito il relativo metodo up

public function up()
{
    Schema::create('car_user', function (Blueprint $table) {
        $table->unsignedBigInteger('car_id');
        $table->foreign('car_id')->references('id')->on('cars')->onDelete('cascade');
        $table->unsignedBigInteger('user_id');
        $table->foreign('user_id')->references('id')->on('users');
        $table->timestamp('ended_at')->nullable();
        $table->timestamps();
    });
}

Ora da terminale lanciamo il comando per lanciare le migration

php artisan migrate

Perfetto, il nostro database è stato creato. Ma prima di passare oltre, vediamo cosa succede se creiamo una nuova migration e successivamente lanciamo il comando per migrare lo schema.

php artisan make:migration create_tests_table
php artisan migrate

Come possiamo vedere, Laravel ha lanciato solo l’ultima migration, in quanto le altre erano già state lanciate. Questo è possibile perché all’interno del nostro database c’è una tabella chiamata migrations, dove il framework memorizza i batch delle migrazioni.

migrazioni progressive

Appreso questo concetto, dobbiamo ripristinare il versionamento alla situazione precedente. Ci basterà invocare il comando rollback delle migrations e saranno cancellate solo le tabelle relative all’ultimo batch.

php artisan migrate:rollback

Cancelliamo il file {prefix}_create_tests_table.php e passiamo alla lezione successiva.