UUID come chiave primaria in Laravel

Nelle applicazioni web moderne, la scelta della strategia per le chiavi primarie ha un impatto significativo sulla sicurezza, sulla scalabilità e sull'architettura generale del sistema. Laravel, fin dalle versioni recenti, offre un supporto nativo ed elegante per l'utilizzo degli UUID (Universally Unique Identifier) come chiavi primarie al posto dei tradizionali interi auto-incrementali. In questo articolo viene esaminato il tema in modo approfondito, partendo dai fondamenti teorici fino all'implementazione pratica completa.

Che cosa sono gli UUID

Un UUID è un identificatore a 128 bit, solitamente rappresentato come una stringa esadecimale suddivisa in cinque gruppi separati da trattini, nella forma xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. Lo standard è definito nella specifica RFC 4122 e prevede diverse versioni, ciascuna con un algoritmo di generazione differente.

Le versioni più utilizzate nel contesto delle applicazioni web sono le seguenti:

  • UUID v1: basato sull'ora corrente e sull'indirizzo MAC della macchina. Garantisce unicità temporale ma può esporre informazioni sull'hardware.
  • UUID v4: generato in modo casuale. È il più diffuso e quello supportato nativamente da Laravel tramite il metodo Str::uuid().
  • UUID v7: introdotto più recentemente, combina un timestamp ordinabile con componenti casuali. Laravel lo supporta tramite Str::orderedUuid(), che genera un ULID compatibile con la struttura UUID ordinabile.

Un esempio di UUID v4 è il seguente: 550e8400-e29b-41d4-a716-446655440000.

UUID vs chiavi intere auto-incrementali

Prima di procedere con l'implementazione, è utile comprendere i vantaggi e gli svantaggi degli UUID rispetto agli ID interi tradizionali.

Vantaggi degli UUID

Il principale vantaggio degli UUID è l'unicità globale. Un UUID generato su qualsiasi macchina, in qualsiasi momento, è praticamente garantito essere unico nell'universo degli identificatori possibili. Questo li rende ideali per sistemi distribuiti, microservizi, e scenari in cui più database o servizi devono essere sincronizzati o uniti senza conflitti di chiavi.

Dal punto di vista della sicurezza, gli UUID v4 non espongono informazioni sulla struttura interna del database. Con gli ID interi auto-incrementali, un attaccante che conosce l'ID di una risorsa può facilmente enumerare le risorse esistenti (ad esempio provando ID 1, 2, 3, ...). Con gli UUID questo tipo di attacco per enumerazione diventa praticamente impossibile.

Gli UUID permettono inoltre di generare l'identificatore lato client o lato applicazione prima di eseguire l'inserimento nel database. Questo semplifica alcune architetture, come quelle event-sourced o CQRS, in cui l'identità di un'entità deve essere nota prima della sua persistenza.

Svantaggi degli UUID

Il principale svantaggio è la dimensione. Un UUID occupa 36 caratteri come stringa o 16 byte in formato binario, contro i 4 o 8 byte di un intero. Questo ha un impatto sullo spazio di storage degli indici e sulle prestazioni delle query che coinvolgono join o ricerche per chiave primaria.

Gli UUID v4, essendo completamente casuali, causano frammentazione degli indici B-tree. Ogni nuovo record viene inserito in una posizione casuale nell'indice invece che in fondo ad esso, causando frequenti operazioni di split delle pagine e degradazione delle prestazioni in scrittura su tabelle con molti record. Questo problema viene mitigato dagli UUID ordinabili (v7 o ULID), che mantengono un ordinamento temporale.

Configurazione del progetto Laravel

Per questo articolo si utilizza Laravel 11 o superiore. Si suppone di avere un progetto Laravel già inizializzato con Composer e una connessione al database configurata nel file .env.

La configurazione del database nel file .env è la seguente:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_uuid_demo
DB_USERNAME=root
DB_PASSWORD=secret

In alternativa è possibile usare SQLite per lo sviluppo locale:

DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/database.sqlite

Il trait HasUuids

A partire da Laravel 9, il framework include il trait HasUuids nello spazio dei nomi Illuminate\Database\Eloquent\Concerns. Questo trait, applicato a un modello Eloquent, istruisce Laravel a:

  • Generare automaticamente un UUID v4 per la chiave primaria al momento della creazione del record.
  • Disabilitare l'auto-incremento della chiave primaria.
  • Trattare la chiave primaria come una stringa invece che come un intero.

Vediamo come utilizzarlo nella pratica.

Creazione della migration

La migration deve definire la colonna della chiave primaria come stringa di lunghezza 36 (la lunghezza standard di un UUID formattato) oppure come tipo nativo uuid supportato da MySQL 8 e PostgreSQL.

php artisan make:migration create_articles_table

Il file di migration generato viene modificato come segue:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

// Migrazione per la tabella degli articoli con UUID come chiave primaria
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('articles', function (Blueprint $table) {
            // Definisce la colonna id come UUID, non auto-incrementale
            $table->uuid('id')->primary();
            $table->string('title');
            $table->text('content');
            $table->string('slug')->unique();
            $table->foreignUuid('author_id')->constrained('users');
            $table->boolean('published')->default(false);
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('articles');
    }
};

Il metodo $table->uuid('id') crea una colonna di tipo CHAR(36) su MySQL o UUID su PostgreSQL. Il metodo $table->foreignUuid('author_id') è il metodo di convenienza di Laravel per definire una chiave esterna di tipo UUID.

Per eseguire la migration:

php artisan migrate

Creazione del modello con HasUuids

Il modello Article viene creato con il seguente comando:

php artisan make:model Article

Il modello viene quindi modificato per includere il trait HasUuids:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

// Modello per gli articoli del blog con UUID come chiave primaria
class Article extends Model
{
    use HasFactory, HasUuids;

    // Attributi assegnabili in massa
    protected $fillable = [
        'title',
        'content',
        'slug',
        'author_id',
        'published',
        'published_at',
    ];

    // Casting dei tipi per gli attributi
    protected $casts = [
        'published' => 'boolean',
        'published_at' => 'datetime',
    ];

    // Relazione con l'autore dell'articolo
    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'author_id');
    }
}

Non è necessario specificare $incrementing = false$keyType = 'string' perché il trait HasUuids li configura automaticamente nel suo metodo initializeHasUuids().

Personalizzare le colonne gestite da HasUuids

Per impostazione predefinita, HasUuids gestisce solo la colonna della chiave primaria. È possibile estendere questo comportamento per assegnare UUID automaticamente anche ad altre colonne, sovrascrivendo il metodo uniqueIds() nel modello:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;

// Modello con UUID automatico su più colonne
class Order extends Model
{
    use HasUuids;

    protected $fillable = [
        'public_token',
        'customer_id',
        'total_amount',
        'status',
    ];

    // Specifica le colonne che devono ricevere un UUID automatico
    public function uniqueIds(): array
    {
        return ['id', 'public_token'];
    }
}

In questo caso, sia la chiave primaria id sia la colonna public_token ricevono un UUID generato automaticamente ad ogni creazione del record.

UUID ordinabili con HasUlids

Laravel offre anche il trait HasUlids, che genera identificatori ULID (Universally Unique Lexicographically Sortable Identifier) invece di UUID v4. Un ULID è ordinabile lessicograficamente per tempo di creazione, il che risolve il problema della frammentazione degli indici tipico degli UUID v4 casuali.

Un ULID si presenta in questo formato: 01ARZ3NDEKTSV4RRFFQ69G5FAV. È composto da 26 caratteri in codifica Base32 e include un timestamp a 48 bit nei bit più significativi.

php artisan make:migration create_events_table
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

// Migrazione per la tabella degli eventi con ULID come chiave primaria
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('events', function (Blueprint $table) {
            // Il tipo ulid genera una colonna CHAR(26)
            $table->ulid('id')->primary();
            $table->string('name');
            $table->json('payload')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('events');
    }
};
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;

// Modello per gli eventi di sistema con ULID come chiave primaria
class Event extends Model
{
    use HasUlids;

    protected $fillable = [
        'name',
        'payload',
    ];

    protected $casts = [
        'payload' => 'array',
    ];
}

Relazioni Eloquent con chiavi UUID

Le relazioni Eloquent funzionano con gli UUID esattamente come con gli interi, a patto che le colonne delle chiavi esterne siano definite con il tipo corretto nella migration e che il tipo della chiave sia coerente tra le tabelle correlate.

Si consideri un sistema con utenti, articoli e commenti, tutti con UUID come chiave primaria:

php artisan make:migration create_comments_table
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

// Migrazione per la tabella dei commenti con chiavi UUID
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->uuid('id')->primary();
            // Chiave esterna UUID verso la tabella articles
            $table->foreignUuid('article_id')->constrained()->cascadeOnDelete();
            // Chiave esterna UUID verso la tabella users
            $table->foreignUuid('user_id')->constrained()->cascadeOnDelete();
            $table->text('body');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('comments');
    }
};
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

// Modello per i commenti agli articoli
class Comment extends Model
{
    use HasUuids;

    protected $fillable = [
        'article_id',
        'user_id',
        'body',
    ];

    // Relazione con l'articolo associato al commento
    public function article(): BelongsTo
    {
        return $this->belongsTo(Article::class);
    }

    // Relazione con l'utente autore del commento
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Nel modello Article si aggiunge la relazione inversa:

use Illuminate\Database\Eloquent\Relations\HasMany;

// Relazione uno-a-molti con i commenti dell'articolo
public function comments(): HasMany
{
    return $this->hasMany(Comment::class);
}

Modificare il modello User per utilizzare UUID

In molti progetti è necessario che anche il modello User predefinito di Laravel utilizzi UUID. Questo richiede di modificare sia la migration che il modello.

Prima di tutto, si modifica la migration degli utenti. In Laravel 11 questa si trova in database/migrations/0001_01_01_000000_create_users_table.php:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

// Migrazione modificata per utilizzare UUID nella tabella utenti
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            // Sostituisce $table->id() con uuid
            $table->uuid('id')->primary();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });

        Schema::create('password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });

        Schema::create('sessions', function (Blueprint $table) {
            $table->string('id')->primary();
            // Colonna user_id come UUID nullable per le sessioni anonime
            $table->foreignUuid('user_id')->nullable()->index();
            $table->string('ip_address', 45)->nullable();
            $table->text('user_agent')->nullable();
            $table->longText('payload');
            $table->integer('last_activity')->index();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('users');
        Schema::dropIfExists('password_reset_tokens');
        Schema::dropIfExists('sessions');
    }
};

Il modello User viene quindi aggiornato:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

// Modello utente con UUID come chiave primaria
class User extends Authenticatable
{
    use HasFactory, HasUuids, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    // Articoli scritti dall'utente
    public function articles(): HasMany
    {
        return $this->hasMany(Article::class, 'author_id');
    }

    // Commenti scritti dall'utente
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}

Factory e Seeder con UUID

Le factory di Laravel funzionano correttamente con i modelli che utilizzano HasUuids, poiché la generazione dell'UUID avviene automaticamente durante la creazione del modello. Non è necessario specificare l'ID nella definizione della factory.

php artisan make:factory ArticleFactory --model=Article
<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

// Factory per la generazione di articoli di test
class ArticleFactory extends Factory
{
    public function definition(): array
    {
        $title = $this->faker->sentence(6);

        return [
            // Non è necessario specificare 'id': viene generato dal trait HasUuids
            'title' => $title,
            'content' => $this->faker->paragraphs(5, true),
            // Genera uno slug univoco dal titolo
            'slug' => Str::slug($title) . '-' . $this->faker->unique()->randomNumber(4),
            'author_id' => User::factory(),
            'published' => $this->faker->boolean(70),
            'published_at' => $this->faker->optional(0.7)->dateTimeBetween('-1 year', 'now'),
        ];
    }

    // Stato per articoli pubblicati
    public function published(): static
    {
        return $this->state(fn (array $attributes) => [
            'published' => true,
            'published_at' => now(),
        ]);
    }

    // Stato per articoli non pubblicati
    public function draft(): static
    {
        return $this->state(fn (array $attributes) => [
            'published' => false,
            'published_at' => null,
        ]);
    }
}

Il seeder che utilizza le factory:

<?php

namespace Database\Seeders;

use App\Models\Article;
use App\Models\Comment;
use App\Models\User;
use Illuminate\Database\Seeder;

// Seeder principale per popolare il database con dati di test
class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // Crea 10 utenti con i relativi articoli e commenti
        User::factory(10)->create()->each(function (User $user) {
            // Ogni utente ha tra 2 e 5 articoli
            $articles = Article::factory(rand(2, 5))
                ->published()
                ->for($user, 'author')
                ->create();

            // Ogni articolo riceve tra 0 e 10 commenti da utenti casuali
            $articles->each(function (Article $article) {
                Comment::factory(rand(0, 10))->create([
                    'article_id' => $article->id,
                    'user_id' => User::inRandomOrder()->first()->id,
                ]);
            });
        });
    }
}

Per eseguire il seeder:

php artisan db:seed

Route model binding con UUID

Il route model binding di Laravel funziona automaticamente con i modelli che utilizzano UUID come chiave primaria, poiché Eloquent sa come risolvere il binding usando il metodo find() sulla chiave primaria.

<?php

use App\Http\Controllers\ArticleController;
use Illuminate\Support\Facades\Route;

// Definizione delle route per la gestione degli articoli
Route::prefix('articles')->name('articles.')->group(function () {
    Route::get('/', [ArticleController::class, 'index'])->name('index');
    // Il parametro {article} viene risolto automaticamente tramite UUID
    Route::get('/{article}', [ArticleController::class, 'show'])->name('show');
    Route::post('/', [ArticleController::class, 'store'])->name('store');
    Route::put('/{article}', [ArticleController::class, 'update'])->name('update');
    Route::delete('/{article}', [ArticleController::class, 'destroy'])->name('destroy');
});

Il controller corrispondente:

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreArticleRequest;
use App\Http\Requests\UpdateArticleRequest;
use App\Http\Resources\ArticleResource;
use App\Models\Article;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

// Controller per la gestione delle risorse Article
class ArticleController extends Controller
{
    // Restituisce l'elenco paginato degli articoli pubblicati
    public function index(): AnonymousResourceCollection
    {
        $articles = Article::with('author')
            ->where('published', true)
            ->orderBy('published_at', 'desc')
            ->paginate(15);

        return ArticleResource::collection($articles);
    }

    // Restituisce un singolo articolo tramite UUID risolto dal route binding
    public function show(Article $article): ArticleResource
    {
        $article->load('author', 'comments.user');

        return new ArticleResource($article);
    }

    // Crea un nuovo articolo e restituisce la risorsa creata
    public function store(StoreArticleRequest $request): JsonResponse
    {
        $article = Article::create($request->validated());

        return (new ArticleResource($article))
            ->response()
            ->setStatusCode(201);
    }

    // Aggiorna un articolo esistente
    public function update(UpdateArticleRequest $request, Article $article): ArticleResource
    {
        $article->update($request->validated());

        return new ArticleResource($article);
    }

    // Elimina un articolo dal database
    public function destroy(Article $article): JsonResponse
    {
        $article->delete();

        return response()->json(null, 204);
    }
}

API Resource con UUID

Le API Resource di Laravel gestiscono gli UUID come stringhe. È importante ricordare che la risposta JSON includerà l'UUID come stringa, non come intero:

php artisan make:resource ArticleResource
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

// Risorsa API per la trasformazione del modello Article in JSON
class ArticleResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            // L'UUID viene restituito come stringa nel formato standard
            'id' => $this->id,
            'title' => $this->title,
            'slug' => $this->slug,
            'content' => $this->content,
            'published' => $this->published,
            'published_at' => $this->published_at?->toISOString(),
            // Carica l'autore solo se la relazione è già stata caricata
            'author' => new UserResource($this->whenLoaded('author')),
            // Carica i commenti solo se la relazione è già stata caricata
            'comments' => CommentResource::collection($this->whenLoaded('comments')),
            'created_at' => $this->created_at->toISOString(),
            'updated_at' => $this->updated_at->toISOString(),
        ];
    }
}

Validazione degli UUID nelle request

Laravel include la regola di validazione uuid che verifica se il valore fornito è un UUID valido nel formato RFC 4122. Questa regola è utile nei Form Request per validare gli UUID passati come parametri nelle richieste:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

// Request per la creazione di un nuovo articolo
class StoreArticleRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user() !== null;
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'content' => ['required', 'string'],
            'slug' => ['required', 'string', 'max:255', 'unique:articles,slug'],
            // Valida che author_id sia un UUID valido e che esista nella tabella users
            'author_id' => ['required', 'uuid', 'exists:users,id'],
            'published' => ['boolean'],
            'published_at' => ['nullable', 'date'],
        ];
    }

    public function messages(): array
    {
        return [
            'author_id.uuid' => 'Il campo author_id deve essere un UUID valido.',
            'author_id.exists' => 'L\'autore specificato non esiste nel sistema.',
        ];
    }
}

Testing con UUID

I test con Pest o PHPUnit funzionano senza modifiche particolari quando si utilizzano modelli con UUID. Le factory generano automaticamente UUID univoci per ogni istanza. Di seguito un esempio di test con Pest:

<?php

use App\Models\Article;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;

// Test per le funzionalità relative agli articoli
uses(RefreshDatabase::class);

// Verifica che un articolo venga creato con un UUID valido
it('creates an article with a valid UUID primary key', function () {
    $user = User::factory()->create();
    $article = Article::factory()->for($user, 'author')->create();

    // Verifica che la chiave primaria sia una stringa
    expect($article->id)->toBeString();

    // Verifica che la chiave primaria sia un UUID valido secondo RFC 4122
    expect(Str::isUuid($article->id))->toBeTrue();
});

// Verifica che due articoli abbiano UUID diversi
it('generates unique UUIDs for different articles', function () {
    $user = User::factory()->create();
    $firstArticle = Article::factory()->for($user, 'author')->create();
    $secondArticle = Article::factory()->for($user, 'author')->create();

    expect($firstArticle->id)->not->toBe($secondArticle->id);
});

// Verifica che un articolo sia recuperabile tramite UUID
it('retrieves an article by UUID', function () {
    $user = User::factory()->create();
    $article = Article::factory()->for($user, 'author')->create();

    $found = Article::find($article->id);

    expect($found)->not->toBeNull();
    expect($found->id)->toBe($article->id);
});

// Verifica che la route di dettaglio funzioni con UUID nel percorso
it('resolves article route model binding with UUID', function () {
    $user = User::factory()->create();
    $article = Article::factory()->published()->for($user, 'author')->create();

    $response = $this->getJson("/api/articles/{$article->id}");

    $response->assertOk()
        ->assertJsonFragment(['id' => $article->id]);
});

// Verifica che una richiesta con UUID non valido restituisca 404
it('returns 404 for invalid UUID in route', function () {
    $response = $this->getJson('/api/articles/not-a-valid-uuid');

    $response->assertNotFound();
});

// Verifica che le chiavi esterne UUID siano gestite correttamente
it('handles UUID foreign keys in relationships', function () {
    $user = User::factory()->create();
    $article = Article::factory()->for($user, 'author')->create();

    // Verifica che la chiave esterna author_id sia l'UUID corretto
    expect($article->author_id)->toBe($user->id);
    expect(Str::isUuid($article->author_id))->toBeTrue();
});

Considerazioni sulle prestazioni con MySQL

Quando si utilizzano UUID su MySQL, è consigliabile memorizzarli in formato binario per ridurre le dimensioni dell'indice e migliorare le prestazioni. MySQL 8 offre le funzioni UUID_TO_BIN() e BIN_TO_UUID() per questo scopo, ma la loro integrazione con Eloquent richiede un approccio personalizzato tramite un cast dedicato.

Un approccio alternativo, compatibile con tutti i database e senza richiedere cast personalizzati, è l'utilizzo degli ULID tramite il trait HasUlids, che offre identificatori ordinabili memorizzati come CHAR(26).

Per applicazioni ad alto volume di scrittura su MySQL con UUID v4, si consideri la seguente migration che usa il tipo binario:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

// Migrazione ottimizzata per UUID in formato binario su MySQL
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('high_volume_records', function (Blueprint $table) {
            // UUID memorizzato come BINARY(16) per ottimizzare le dimensioni dell'indice
            $table->binary('id', 16)->primary();
            $table->string('payload');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('high_volume_records');
    }
};

In questo caso è necessario implementare un cast personalizzato nel modello per gestire la conversione tra formato stringa e binario, oppure utilizzare un pacchetto di terze parti come laravel-binary-uuid.

Per la maggior parte delle applicazioni, tuttavia, la differenza di prestazioni tra CHAR(36) e BINARY(16) è trascurabile fino a tabelle con decine di milioni di record, e il tipo uuid nativo di Laravel offre il miglior equilibrio tra semplicità e compatibilità.

Conclusione

L'utilizzo degli UUID come chiavi primarie in Laravel è oggi una pratica supportata nativamente e ben integrata nell'ecosistema del framework. I trait HasUuids e HasUlids eliminano la necessità di configurazioni manuali complesse, rendendo l'adozione di questa strategia semplice e trasparente per sviluppatori di qualsiasi livello.

La scelta tra UUID v4 tramite HasUuids e ULID tramite HasUlids dipende dal caso d'uso specifico: gli UUID v4 sono lo standard universalmente riconosciuto e ideali per l'interoperabilità, mentre gli ULID offrono il vantaggio aggiuntivo dell'ordinamento temporale che preserva l'efficienza degli indici. In entrambi i casi, la migrazione da chiavi intere a identificatori univoci globali porta benefici concreti in termini di sicurezza e architettura, specialmente in sistemi distribuiti o che necessitano di sincronizzazione tra fonti di dati diverse.