Композиция транзакций
Безопасно объединяйте эффекты.
«Композиция транзакций» — бесплатный урок 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 — локальная установка не требуется.
Все уроки этого курса
- Подключение через трансактор
- Выполнение запросов
- Вставка и обновление
- Композиция транзакций