0Pricing
Scala for Backend Engineering & Functional Programming · Aula

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 id

Uma 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 n

Reutilizando 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) *> program

Tentando 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.run

Verificaçã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

  1. Conectando com um transator
  2. Executando consultas
  3. Inserções e atualizações
  4. Compondo transações
← Voltar para Scala for Backend Engineering & Functional Programming