MENU
Eloquent come Dio Comanda: Transazioni Database in Laravel - Guida Completa

Le transazioni SQL rappresentano un meccanismo cruciale nella gestione dei database moderni, garantendo l’elaborazione affidabile e coerente delle operazioni complesse. Nel contesto dello sviluppo web contemporaneo, Laravel offre strumenti avanzati per implementare transazioni in modo efficace ed elegante attraverso il suo ORM Eloquent.

Cosa Sono le Transazioni?

Una transazione SQL è un gruppo di comandi trattati come un’unità di lavoro indivisibile. L’obiettivo principale è assicurare che:

  • Un insieme di operazioni venga eseguito completamente o per nulla
  • Si evitino stati intermedi parziali che potrebbero compromettere l’integrità dei dati
  • Si gestiscano operazioni complesse in modo sicuro e controllato

Implementazione Base delle Transazioni in Laravel

Laravel offre diversi metodi per gestire le transazioni, semplificando notevolmente il processo per gli sviluppatori.

Metodo Statico: DB::transaction()

use Illuminate\\Support\\Facades\\DB;

DB::transaction(function () {
    // Qui inserisci le tue operazioni database
    User::create(['name' => 'Mario']);
    Profile::create(['user_id' => $user->id]);
});

Metodo Manuale: beginTransaction(), commit(), rollBack()

try {
    DB::beginTransaction();

    // Operazioni database
    $user = User::create(['name' => 'Luigi']);
    $profile = Profile::create(['user_id' => $user->id]);

    DB::commit();
} catch (\\Exception $e) {
    DB::rollBack();
    // Gestisci l'errore
}

Principio ACID: La Garanzia di Affidabilità

ACID è un acronimo che rappresenta quattro proprietà fondamentali delle transazioni database:

  1. Atomicity (Atomicità): Tutte le operazioni vengono eseguite completamente o per niente
  2. Consistency (Consistenza): Il database passa da uno stato di coerenza a un altro
  3. Isolation (Isolamento): Ogni transazione è indipendente dalle altre
  4. Durability (Durabilità): Le modifiche vengono salvate permanentemente

Ognuno di questi principi gioca un ruolo cruciale nel garantire l’affidabilità e l’integrità delle operazioni sui database.

Dettaglio dei Principi ACID

1. Atomicity (Atomicità)

L’atomicità garantisce che una transazione sia trattata come un’unità indivisibile: o tutte le operazioni vengono completate con successo, o nessuna viene applicata.

Esempio senza Transaction

$sender = Customer::find($this->sender)
					 ->transactions()
					 ->create(['amount' => -$this->amount]);
					 
throw new Exception("ERRORE 0K9: Il cane ha mangiato il cavo del database."); 

$reciever = Customer::find($this->reciever)
					->transactions()
					->create(['amount' => $this->amount]);

In questo scenario, i soldi vengono sottratti dal primo conto, ma non aggiunti al secondo in quanto un errore imprevisto ha bloccato la ricezione del trasferimento.

Nulla si crea, nulla si distrugge, tutto si trasforma ma, in questo caso, il sender ha perso i soldi e il receiver non ha ricevuto nulla.

Esempio con Transaction

//Inizo Blocco Atomico
DB::transaction(function () {
	$sender = Customer::find($this->sender)
						 ->transactions()
						 ->create(['amount' => -$this->amount]);
						 
	throw new Exception("ERRORE 0K9: Il cane ha mangiato il cavo del database."); 
	
	$reciever = Customer::find($this->reciever)
						->transactions()
						->create(['amount' => $this->amount]);
};
//Fine blocco atomico

Con la transaction abilitata se anche solo una delle operazioni fallisce, sono automaticamente tutte annullate.

2. Consistency (Consistenza)

La consistenza assicura che una transazione porti il database da uno stato valido a un altro stato valido, rispettando tutti i vincoli definiti.

Esempio senza Transaction

  $customer = Customer::create([
      'firstname' => $this->firstname,
      'lastname' => $this->lastname
  ]);
  
	throw new Exception("ERRORE 0K9: Il cane ha mangiato il cavo del database."); 
	
  Account::create([
      'customer_id' => $customer->id,
      'bank' => $this->bank,
      'iban' => str(fake()->iban('IT')),
  ]);

L’utente verrebbe creato senza profilo bancario, violando la consistenza del modello in quanto avremmo un cliente senza codice IBAN associato.

Esempio con Transaction

DB::transaction(function() {
  $customer = Customer::create([
      'firstname' => $this->firstname,
      'lastname' => $this->lastname
  ]);
  
	throw new Exception("ERRORE 0K9: Il cane ha mangiato il cavo del database."); 
	
  Account::create([
      'customer_id' => $customer->id,
      'bank' => $this->bank,
      'iban' => str(fake()->iban('IT')),
  ])
  
});

L’account, se non viene generato, annullerà anche la creazione del cliente in quanto avere un dato inconsistente violerebbe uno dei principi di un database relazionali.

3. Isolation (Isolamento)

L’isolamento garantisce che le transazioni contemporanee non interferiscano tra loro, assicurando che ogni transazione sia eseguita come se fosse l’unica in esecuzione in quel momento.

Esempio senza Transaction

//Prima transazione
$sender = Customer::find($this->sender);
if ($sender->balance < $this->amount) {
    $this->addError('transaction_error', 'Saldo insufficiente per eseguire la transazione');
    return;
}
sleep(10);//Simuliamo un delay di 10 secondi a seguito di Latenza del DB
$sender->transactions()->create(['amount' => -$this->amount]);
$reciever = Customer::find($this->reciever)->transactions()->create(['amount' => $this->amount]);

//Seconda transazione
$sender = Customer::find($this->sender);
if ($sender->balance < $this->amount) {
    $this->addError('transaction_error', 'Saldo insufficiente per eseguire la transazione');
    return;
}
$sender->transactions()->create(['amount' => -$this->amount]);
$reciever = Customer::find($this->reciever)->transactions()->create(['amount' => $this->amount]);

In questo contesto avremo:

  1. Nella prima transazione viene controllato il bilancio disponibile e, a causa di Latenza o problemi di connessione, ci sono 10 secondi di ritardo in cui l’operazione viene messa in stand-by.
  2. In Contemporanea, parte la seconda transazione dove non ci sono ritardi. Tutto viene prelevato immediatamente.
  3. Sono finalmente trascorsi i 10 secondi, viene effettuato il prelievo portando in negativo il conto in banca!

In definitiva, in questo scenario dove due utenti tentano di prelevare simultaneamente, potrebbero verificarsi problemi di consistenza. Il secondo prelievo potrebbe non tener conto del primo, causando un potenziale scoperto.

Esempio con Transaction


//Prima transazione
DB::transaction(function () use ($delay) {
	$sender = Customer::lockForUpdate()->find($this->sender);

	if ($sender->balance < $this->amount) {
	    $this->addError('transaction_error', 'Saldo insufficiente per eseguire la transazione');
	    return;
	}  
  
	sleep(10); //Simuliamo un delay di 10 secondi a seguito di Latenza del DB

	$sender->transactions()->create(['amount' => -$this->amount]);
  $reciever = Customer::lockForUpdate()->find($this->reciever)->transactions()->create(['amount' => $this->amount]);     
});

//Seconda transazione
DB::transaction(function () use ($delay) {
	$sender = Customer::lockForUpdate()->find($this->sender);

	if ($sender->balance < $this->amount) {
	    $this->addError('transaction_error', 'Saldo insufficiente per eseguire la transazione');
	    return;
	}   
	
	$sender->transactions()->create(['amount' => -$this->amount]);
  $reciever = Customer::lockForUpdate()->find($this->reciever)->transactions()->create(['amount' => $this->amount]);     
});

Con la transaction e il metodo lockForUpdate(), gli altri thread saranno bloccati fino al completamento dell’operazione, garantendo un accesso isolato e sicuro.

In questo contesto avremo:

  1. Nella prima transazione viene controllato il bilancio disponibile e, a causa di Latenza o problemi di connessione, ci sono 10 secondi di ritardo in cui l’operazione viene messa in stand-by.
  2. In Contemporanea, parte la seconda transazione dove non ci sono ritardi. Tutto viene prelevato immediatamente.
  3. Sono finalmente trascorsi i 10 secondi, l’operazione messa in stand-by viene eseguita ma si scontra contro il lockForUpdate().
  4. Riceverai un errore SQLSTATE[HY000]: General error: 5 database is locked in quanto l’isolamento del record in questione è ancora attivo.

4. Durability (Durabilità)

La durabilità garantisce che una volta che una transazione è stata confermata, i suoi cambiamenti siano permanenti e sopravvivano a eventuali guasti di sistema, come interruzioni di corrente o crash del database.

Esempio senza Transaction

$sender = Customer::find($this->sender)
					 ->transactions()
					 ->create(['amount' => -$this->amount]);
					 
$reciever = Customer::find($this->reciever)
					->transactions()
					->create(['amount' => $this->amount]);
					
throw new Exception("ERRORE 0K9: Il cane ha mangiato il cavo del database."); 
//QUi non abbiamo sicurezza al 100% che i dati siano stati salvati correttamente

Esempio con Transaction

DB::transaction(function () use ($delay) {
		$sender = Customer::find($this->sender)
							 ->transactions()
							 ->create(['amount' => -$this->amount]);							 
		
		$reciever = Customer::find($this->reciever)
							->transactions()
							->create(['amount' => $this->amount]);
});
					
throw new Exception("ERRORE 0K9: Il cane ha mangiato il cavo del database."); 
//Se la Transazione ha effettuato il commit, anche se subito dopo il server dovesse andare in crash non importa. QUi  abbiamo sicurezza al 100% che i dati siano stati salvati correttamente.

Transazioni con Eloquent: Casi Avanzati

Transazioni Nidificate

Laravel supporta transazioni nidificate, permettendo operazioni complesse:

DB::transaction(function () {
    // Prima transazione
    $user = User::create(['name' => 'Yoshi']);

    DB::transaction(function () {
        // Transazione nidificata
        $profile = Profile::create(['user_id' => $user->id]);
    });
});

Gestione Errori e Logging

try {
    DB::transaction(function () {
        // Operazioni critiche
        throw new \\Exception('Errore simulato');
    });
} catch (\\Exception $e) {
    Log::error('Transazione fallita: ' . $e->getMessage());
}

Ottimizzazione delle Transazioni: Performance e Sicurezza

Timeout e Blocchi

DB::transaction(function () {
    // Imposta timeout personalizzato
}, 5); // 5 secondi di timeout

Gestione Concorrenza

$user = User::lockForUpdate()->find(1);
// Previene modifiche contemporanee

Anti-Pattern e Controesempi

Cosa NON Fare

// Anti-pattern: Transazione troppo ampia
DB::transaction(function () {
    // Evita operazioni esterne al database qui
    $this->sendEmailNotification(); // Operazione esterna sconsigliata
});

Conclusioni: Best Practice

  1. Mantieni le transazioni brevi e mirate
  2. Gestisci sempre le eccezioni
  3. Utilizza transazioni per operazioni logicamente correlate
  4. Considera l’isolamento e la concorrenza

Conclusione

Le transazioni Eloquent in Laravel offrono un potente strumento per gestire operazioni complesse sui database, garantendo integrità, affidabilità e coerenza dei dati.