Compondo transações
Combine efeitos com segurança.
Compondo transações é uma aula grátis de Scala for Backend Engineering & Functional Programming no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Scala for Backend Engineering & Functional Programming, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Scala for Backend Engineering & Functional Programming inclui 4 aulas no total.
ConnectionIO é uma mônada
O verdadeiro poder do Doobie está no fato de ConnectionIO ser uma mônada. Você combina várias instruções em um programa maior usando flatMap ou uma compreensão for.
Tudo o que você encadeia dessa forma é executado na mesma conexão, dentro de uma única transação.
import doobie.implicits._
val program: ConnectionIO[Long] =
for {
id <- insertUser("Ada")
_ <- insertProfile(id)
} yield idUma transação por transact
Independentemente de quantas instruções você componha, todo o ConnectionIO se torna uma única transação quando você chama .transact.
Se alguma etapa falhar, a estratégia do Transactor desfaz tudo; se todas forem concluídas com sucesso, ele confirma a transação uma única vez, no final.
val io: IO[Long] = program.transact(xa)Gravações atômicas com várias instruções
Como as instruções compostas compartilham uma transação, você obtém atomicidade automaticamente. Uma transferência que debita uma conta e credita outra ou é concluída por inteiro ou não acontece.
Não haverá estado parcial se a segunda atualização lançar um erro.
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 ()Lançando e tratando erros
ConnectionIO tem uma instância de MonadError, portanto você pode usar raiseError para interromper uma transação e acionar o rollback.
Você também pode usar handleErrorWith para se recuperar, mas observe que a recuperação, por si só, não desfaz instruções anteriores, a menos que a transação sofra 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"))
}Lógica condicional em uma transação
Como ele é apenas uma mônada, o fluxo de controle comum do Scala funciona. Você pode ler uma linha, tomar uma decisão com base no valor e gravar de acordo com ele, tudo de forma atômica.
Isso mantém as regras de negócio e a persistência em uma única unidade consistente.
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 ()Pontos de salvamento para rollback parcial
Para ter um controle mais preciso, o Doobie expõe os Savepoints do JDBC por meio da álgebra de conexão livre FC. Você pode voltar a um ponto de salvamento sem interromper toda a transação.
Isso permite padrões de tentativa e alternativa dentro de uma única transação externa.
import doobie.free.{connection => FC}
val withSp =
for {
sp <- FC.setSavepoint
_ <- riskyWrite.handleErrorWith(_ => FC.rollback(sp))
} yield ()Combinando consultas e gravações
Leituras e gravações podem ser combinadas livremente no mesmo programa. Você pode selecionar o estado atual, calcular uma alteração no Scala e persistir o resultado, tudo na mesma conexão.
A leitura enxerga as alterações ainda não confirmadas feitas anteriormente na mesma transação.
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 nReutilizando programas com traverse
Como os programas são valores, você pode executar o mesmo programa sobre uma lista com o traverse do cats, encadeando as operações em uma única transação.
Todas as inserções são confirmadas juntas, ou nenhuma delas é confirmada.
import cats.syntax.all._
def saveAll(names: List[String]): ConnectionIO[List[Long]] =
names.traverse(insertUser)Níveis de isolamento
Você pode definir o nível de isolamento da transação dentro de um programa, usando a álgebra de conexão antes de realizar o trabalho.
Níveis mais rígidos, como SERIALIZABLE, evitam anomalias, mas podem fazer o banco de dados interromper transações conflitantes; nesse caso, você deve tentar novamente.
import doobie.free.{connection => FC}
import java.sql.Connection
val serializable =
FC.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) *> programTentando novamente após falhas de serialização
As novas tentativas pertencem à camada de efeitos, não à transação. Depois de .transact, você tem um IO e pode tentar novamente toda a transação quando ocorrer um erro de serialização.
Cada nova tentativa é uma transação nova e independente.
def runWithRetry(io: IO[Unit], n: Int): IO[Unit] =
io.handleErrorWith {
case _ if n > 0 => runWithRetry(io, n - 1)
case e => IO.raiseError(e)
}Mantenha as transações curtas
Componha em um único ConnectionIO apenas o trabalho que precisa ser atômico. Não faça chamadas externas (HTTP ou operações demoradas da CPU) dentro de uma transação; isso mantém uma conexão e bloqueios ocupados.
Leia, faça os cálculos fora da transação se possível e depois grave em uma transação curta.
// good: gather input first, then one short transactional write
val write: ConnectionIO[Int] =
sql"update users set name = $name where id = $id".update.runVerificação rápida
Raciocine sobre os limites das transações.
Recapitulação
ConnectionIO é uma mônada, portanto as compreensões for compõem instruções em uma única transação atômica por .transact. Os erros desfazem tudo; raiseError interrompe a transação deliberadamente.
Use pontos de salvamento para rollback parcial, defina os níveis de isolamento dentro do programa, tente novamente as falhas de serialização na camada de efeitos e mantenha as transações curtas.
Perguntas Frequentes
A aula “Compondo transações” é grátis?
Sim — o texto completo de “Compondo transações” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Scala for Backend Engineering & Functional Programming, atualize para CoddyKit PRO. O curso de Scala for Backend Engineering & Functional Programming inclui 4 aulas no total.
O que vou aprender em “Compondo transações”?
Combine efeitos com segurança. Você pratica Scala for Backend Engineering & Functional Programming com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar Scala for Backend Engineering & Functional Programming?
Nenhuma experiência prévia é necessária. Scala for Backend Engineering & Functional Programming no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.
Quanto tempo leva a aula “Compondo transações”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de Scala for Backend Engineering & Functional Programming?
Sim. Cada aula de Scala for Backend Engineering & Functional Programming inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Conectando com um transator
- Executando consultas
- Inserções e atualizações
- Compondo transações