Comporre transazioni
Combini gli effetti in sicurezza.
Comporre transazioni è una lezione Scala for Backend Engineering & Functional Programming gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Scala for Backend Engineering & Functional Programming, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Scala for Backend Engineering & Functional Programming include 4 lezioni in totale.
ConnectionIO è un monade
La vera potenza di Doobie sta nel fatto che ConnectionIO è un monade. È possibile combinare molte istruzioni in un programma più ampio usando flatMap o una for-comprehension.
Tutto ciò che si concatena in questo modo viene eseguito sulla stessa connessione all'interno di un'unica transazione.
import doobie.implicits._
val program: ConnectionIO[Long] =
for {
id <- insertUser("Ada")
_ <- insertProfile(id)
} yield idUna transazione per ogni transact
Indipendentemente dal numero di istruzioni composte, l'intera ConnectionIO diventa un'unica transazione quando si chiama .transact.
Se un passaggio fallisce, la strategia del Transactor annulla tutto; se tutti i passaggi riescono, esegue il commit una sola volta alla fine.
val io: IO[Long] = program.transact(xa)Scritture atomiche con più istruzioni
Poiché le istruzioni composte condividono una transazione, l'atomicità è garantita automaticamente. Un trasferimento che addebita un conto e accredita un altro viene completato interamente oppure non viene eseguito affatto.
Se il secondo aggiornamento genera un'eccezione, non rimane alcuno stato parziale.
def transfer(from: Long, to: Long, cents: Int) =
for {
_ <- sql"update acct set bal = bal - $cents where id = $from".update.run
_ <- sql"update acct set bal = bal + $cents where id = $to".update.run
} yield ()Generare e gestire gli errori
ConnectionIO dispone di un'istanza MonadError, quindi è possibile usare raiseError per interrompere una transazione e attivare il rollback.
È anche possibile usare handleErrorWith per recuperare dall'errore, ma il recupero da solo non annulla le istruzioni precedenti, a meno che la transazione non venga sottoposta a rollback.
import cats.syntax.all._
import doobie.free.connection.{raiseError, pure}
def debit(id: Long, c: Int): ConnectionIO[Unit] =
sql"update acct set bal = bal - $c where id = $id".update.run.flatMap {
case 1 => pure(())
case _ => raiseError(new RuntimeException("no such account"))
}Logica condizionale in una transazione
Essendo semplicemente un monade, supporta il normale flusso di controllo Scala. È possibile leggere una riga, diramare in base al suo valore e scrivere di conseguenza, il tutto in modo atomico.
In questo modo le regole di business e la persistenza restano in un'unica unità coerente.
for {
bal <- sql"select bal from acct where id = $id".query[Int].unique
_ <- if (bal >= amt)
sql"update acct set bal = bal - $amt where id = $id".update.run
else raiseError(new RuntimeException("insufficient funds"))
} yield ()Savepoint per il rollback parziale
Per un controllo più preciso, Doobie espone i Savepoint JDBC tramite l'algebra FC (free connection). È possibile eseguire il rollback fino a un savepoint senza interrompere l'intera transazione.
Questo consente di usare schemi try/fallback all'interno di un'unica transazione esterna.
import doobie.free.{connection => FC}
val withSp =
for {
sp <- FC.setSavepoint
_ <- riskyWrite.handleErrorWith(_ => FC.rollback(sp))
} yield ()Combinare query e scritture
Le letture e le scritture possono essere combinate liberamente nello stesso programma. È possibile selezionare lo stato corrente, calcolare una modifica in Scala e renderla persistente, tutto sulla stessa connessione.
La lettura vede le modifiche non ancora sottoposte a commit effettuate in precedenza nella stessa transazione.
for {
v <- sql"select stock from items where id = $id".query[Int].unique
_ <- sql"update items set stock = ${v - 1} where id = $id".update.run
n <- sql"select stock from items where id = $id".query[Int].unique
} yield nRiutilizzare i programmi con traverse
Poiché i programmi sono valori, è possibile eseguire lo stesso programma su una lista con traverse di cats, concatenando le operazioni in un'unica transazione.
Tutti gli inserimenti vengono sottoposti a commit insieme, oppure nessuno di essi lo viene.
import cats.syntax.all._
def saveAll(names: List[String]): ConnectionIO[List[Long]] =
names.traverse(insertUser)Livelli di isolamento
È possibile impostare il livello di isolamento della transazione all'interno di un programma usando l'algebra della connessione prima di eseguire il lavoro.
Livelli più rigidi come SERIALIZABLE impediscono le anomalie, ma possono indurre il database a interrompere le transazioni in conflitto; in tal caso è necessario riprovare.
import doobie.free.{connection => FC}
import java.sql.Connection
val serializable =
FC.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) *> programRiprovare dopo errori di serializzazione
I tentativi appartengono al livello degli effetti, non alla transazione. Dopo .transact si dispone di un IO e si può ripetere l'intera transazione in caso di errore di serializzazione.
Ogni tentativo è una transazione nuova e indipendente.
def runWithRetry(io: IO[Unit], n: Int): IO[Unit] =
io.handleErrorWith {
case _ if n > 0 => runWithRetry(io, n - 1)
case e => IO.raiseError(e)
}Mantenere brevi le transazioni
Componga in un'unica ConnectionIO solo il lavoro che deve essere atomico. Non esegua chiamate esterne (HTTP, elaborazioni CPU lente) all'interno di una transazione: manterrebbero occupate una connessione e i lock.
Legga, calcoli fuori dalla transazione quando possibile, quindi scriva in una transazione breve e concentrata.
// good: gather input first, then one short transactional write
val write: ConnectionIO[Int] =
sql"update users set name = $name where id = $id".update.runVerifica rapida
Ragioni sui confini delle transazioni.
Riepilogo
ConnectionIO è un monade, quindi le for-comprehension compongono le istruzioni in un'unica transazione atomica per ogni .transact. Gli errori annullano tutto; raiseError interrompe deliberatamente l'esecuzione.
Usi i savepoint per il rollback parziale, imposti i livelli di isolamento all'interno del programma, riprovi gli errori di serializzazione al livello degli effetti e mantenga brevi le transazioni.
Domande Frequenti
La lezione «Comporre transazioni» è gratuita?
Sì — il testo completo di «Comporre transazioni» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Scala for Backend Engineering & Functional Programming, passa a CoddyKit PRO. Il corso Scala for Backend Engineering & Functional Programming include 4 lezioni in totale.
Cosa imparerò in «Comporre transazioni»?
Combini gli effetti in sicurezza. Eserciti Scala for Backend Engineering & Functional Programming con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Scala for Backend Engineering & Functional Programming?
Non è richiesta alcuna esperienza precedente. Scala for Backend Engineering & Functional Programming su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.
Quanto tempo richiede la lezione «Comporre transazioni»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Scala for Backend Engineering & Functional Programming?
Sì. Ogni lezione Scala for Backend Engineering & Functional Programming include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Connettersi con un transactor
- Eseguire query
- Inserimenti e aggiornamenti
- Comporre transazioni