0Pricing
Scala for Backend Engineering & Functional Programming · Урок

Композиция транзакций

Безопасно объединяйте эффекты.

«Композиция транзакций» — бесплатный урок Scala for Backend Engineering & Functional Programming на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Scala for Backend Engineering & Functional Programming, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Scala for Backend Engineering & Functional Programming содержит 4 уроков всего.

ConnectionIO — монада

Главная сила Doobie в том, что ConnectionIO является монадой. С помощью flatMap или for-выражения можно объединить множество инструкций в одну большую программу.

Всё, что вы выстраиваете таким образом, выполняется в одном и том же соединении внутри одной транзакции.

import doobie.implicits._

val program: ConnectionIO[Long] =
  for {
    id <- insertUser("Ada")
    _  <- insertProfile(id)
  } yield id

Одна транзакция на каждый вызов transact

Независимо от количества объединённых инструкций весь ConnectionIO становится одной транзакцией при вызове .transact.

Если какой-либо шаг завершается ошибкой, стратегия Transactor откатывает всё; если все шаги успешны, в конце выполняется одна фиксация.

val io: IO[Long] = program.transact(xa)

Атомарная запись несколькими инструкциями

Поскольку объединённые инструкции используют одну транзакцию, атомарность предоставляется автоматически. Перевод, который списывает средства с одного счёта и зачисляет их на другой, либо выполняется полностью, либо не выполняется совсем.

Если вторая операция обновления выдаст ошибку, частичного состояния не возникнет.

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 ()

Генерация и обработка ошибок

ConnectionIO имеет экземпляр MonadError, поэтому с помощью raiseError можно прервать транзакцию и запустить откат.

Также можно использовать handleErrorWith для восстановления после ошибки, но само по себе восстановление не отменяет предыдущие инструкции, если транзакция не откатывается.

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"))
  }

Условная логика в транзакции

Поскольку это всего лишь монада, обычное управление потоком Scala работает как обычно. Можно прочитать строку, выбрать ветвь по её значению и соответствующим образом выполнить запись — всё атомарно.

Так бизнес-правила и сохранение данных остаются в одной согласованной единице.

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 ()

Точки сохранения для частичного отката

Для более точного управления Doobie предоставляет JDBC-точки сохранения Savepoint через алгебру свободного соединения FC. Можно откатиться к точке сохранения, не прерывая всю транзакцию.

Это позволяет использовать шаблоны попытки и запасного варианта внутри одной внешней транзакции.

import doobie.free.{connection => FC}

val withSp =
  for {
    sp <- FC.setSavepoint
    _  <- riskyWrite.handleErrorWith(_ => FC.rollback(sp))
  } yield ()

Объединение запросов и операций записи

Операции чтения и записи можно свободно смешивать в одной программе. Например, можно выбрать текущее состояние, вычислить изменение в Scala и сохранить его — всё в одном соединении.

Чтение видит незафиксированные изменения, выполненные ранее в рамках той же транзакции.

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

Повторное использование программ с traverse

Поскольку программы являются значениями, одну и ту же программу можно выполнить для списка с помощью traverse из cats, последовательно выполняя все действия в одной транзакции.

Все вставки будут зафиксированы вместе либо не будет зафиксирована ни одна.

import cats.syntax.all._

def saveAll(names: List[String]): ConnectionIO[List[Long]] =
  names.traverse(insertUser)

Уровни изоляции

Уровень изоляции транзакции можно задать внутри программы с помощью алгебры соединения до начала работы.

Более строгие уровни, такие как SERIALIZABLE, предотвращают аномалии, но могут заставить базу данных прервать конфликтующие транзакции; в этом случае транзакцию следует повторить.

import doobie.free.{connection => FC}
import java.sql.Connection

val serializable =
  FC.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) *> program

Повтор при сбоях сериализации

Повторные попытки относятся к слою эффектов, а не к транзакции. После .transact у вас есть IO, и вы можете повторить всю транзакцию при ошибке сериализации.

Каждая попытка выполняется в новой независимой транзакции.

def runWithRetry(io: IO[Unit], n: Int): IO[Unit] =
  io.handleErrorWith {
    case _ if n > 0 => runWithRetry(io, n - 1)
    case e          => IO.raiseError(e)
  }

Сохраняйте транзакции короткими

Объединяйте в один ConnectionIO только работу, которая должна быть атомарной. Не выполняйте внешние вызовы (HTTP, длительные вычисления на CPU) внутри транзакции: это удерживает соединение и блокировки.

По возможности читайте данные и выполняйте вычисления вне транзакции, а затем быстро записывайте результат в рамках короткой транзакции.

// good: gather input first, then one short transactional write
val write: ConnectionIO[Int] =
  sql"update users set name = $name where id = $id".update.run

Быстрая проверка

Рассуждайте о границах транзакций.

Итоги

ConnectionIO — монада, поэтому for-выражения объединяют инструкции в одну атомарную транзакцию на каждый вызов .transact. Ошибки откатывают всё, а raiseError намеренно прерывает выполнение.

Используйте точки сохранения для частичного отката, задавайте уровни изоляции внутри программы, повторяйте транзакции после сбоев сериализации на уровне эффектов и сохраняйте транзакции короткими.

Часто задаваемые вопросы

Урок «Композиция транзакций» бесплатный?

Да — полный текст урока «Композиция транзакций» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Scala for Backend Engineering & Functional Programming, подпишись на CoddyKit PRO. Курс Scala for Backend Engineering & Functional Programming содержит 4 уроков всего.

Чему я научусь в уроке «Композиция транзакций»?

Безопасно объединяйте эффекты. Ты практикуешь Scala for Backend Engineering & Functional Programming с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Scala for Backend Engineering & Functional Programming?

Предыдущий опыт не требуется. Scala for Backend Engineering & Functional Programming на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Композиция транзакций»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Scala for Backend Engineering & Functional Programming?

Да. Каждый урок Scala for Backend Engineering & Functional Programming включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Подключение через трансактор
  2. Выполнение запросов
  3. Вставка и обновление
  4. Композиция транзакций
← Назад к Scala for Backend Engineering & Functional Programming