0Pricing
Scala for Backend Engineering & Functional Programming · Lekcja

Komponowanie transakcji

Bezpiecznie połączy Pan/Pani efekty.

Komponowanie transakcji to bezpłatna lekcja Scala for Backend Engineering & Functional Programming na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Scala for Backend Engineering & Functional Programming, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Scala for Backend Engineering & Functional Programming zawiera 4 lekcji w sumie.

ConnectionIO jest monadą

Prawdziwa siła Doobie polega na tym, że ConnectionIO jest monadą. Wiele instrukcji można połączyć w jeden większy program za pomocą flatMap lub wyrażenia for-comprehension.

Wszystko, co zostanie w ten sposób uporządkowane, działa na tym samym połączeniu w ramach jednej transakcji.

import doobie.implicits._

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

Jedna transakcja na transact

Niezależnie od liczby połączonych instrukcji całe ConnectionIO staje się pojedynczą transakcją po wywołaniu .transact.

Jeśli dowolny krok się nie powiedzie, strategia Transactora wycofa wszystko; jeśli wszystkie kroki zakończą się pomyślnie, na końcu wykona jeden commit.

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

Atomowe zapisy wieloinstrukcyjne

Ponieważ połączone instrukcje współdzielą transakcję, atomowość jest zapewniona automatycznie. Przelew, który obciąża jedno konto i uznaje drugie, albo zostanie wykonany w całości, albo nie zostanie wykonany wcale.

Jeśli druga aktualizacja zgłosi wyjątek, nie pozostanie stan częściowy.

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

Zgłaszanie i obsługa błędów

ConnectionIO ma instancję MonadError, więc można użyć raiseError, aby przerwać transakcję i wywołać rollback.

Można również użyć handleErrorWith do odzyskania po błędzie, ale samo odzyskanie nie cofa wcześniejszych instrukcji, chyba że transakcja zostanie wycofana.

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

Logika warunkowa w transakcji

Ponieważ jest to po prostu monada, działa zwykły przepływ sterowania Scali. Można odczytać wiersz, rozgałęzić działanie na podstawie jego wartości i odpowiednio zapisać wynik — wszystko atomowo.

Dzięki temu reguły biznesowe i utrwalanie danych tworzą jedną spójną jednostkę.

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

Punkty zapisu do częściowego wycofywania

Aby uzyskać dokładniejszą kontrolę, Doobie udostępnia punkty zapisu JDBC Savepoint za pośrednictwem algebry FC (free connection). Można wycofać transakcję do punktu zapisu bez przerywania całej transakcji.

Umożliwia to wzorce próby i awaryjnego rozwiązania w ramach jednej transakcji zewnętrznej.

import doobie.free.{connection => FC}

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

Łączenie zapytań i operacji zapisu

Odczyty i zapisy można swobodnie łączyć w tym samym programie. Można odczytać bieżący stan, obliczyć zmianę w Scali i utrwalić ją — wszystko na jednym połączeniu.

Odczyt widzi niezacommitowane zmiany wykonane wcześniej w ramach tej samej transakcji.

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

Ponowne używanie programów za pomocą traverse

Ponieważ programy są wartościami, można uruchomić ten sam program dla listy za pomocą traverse z cats, porządkując operacje w ramach jednej transakcji.

Wszystkie INSERT-y zostaną zatwierdzone razem albo nie zostanie zatwierdzony żaden.

import cats.syntax.all._

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

Poziomy izolacji

Poziom izolacji transakcji można ustawić wewnątrz programu za pomocą algebry połączenia, przed wykonaniem właściwych operacji.

Bardziej restrykcyjne poziomy, takie jak SERIALIZABLE, zapobiegają anomaliom, ale mogą spowodować przerwanie przez bazę danych kolidujących transakcji — w takim przypadku należy ponowić próbę.

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

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

Ponawianie po błędach serializacji

Ponawianie prób należy do warstwy efektów, a nie do transakcji. Po wywołaniu .transact otrzymuje się IO, więc w przypadku błędu serializacji można ponowić całą transakcję.

Każda próba jest nową, niezależną transakcją.

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

Należy utrzymywać krótkie transakcje

W jednym ConnectionIO należy łączyć tylko operacje, które muszą być atomowe. Nie należy wykonywać zewnętrznych wywołań (HTTP ani powolnych obliczeń CPU) wewnątrz transakcji, ponieważ zajmują one połączenie i blokady.

Jeśli to możliwe, należy odczytać dane i wykonać obliczenia poza transakcją, a następnie przeprowadzić szybki zapis w ramach transakcji.

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

Szybkie sprawdzenie

Należy przeanalizować granice transakcji.

Podsumowanie

ConnectionIO jest monadą, dlatego wyrażenia for-comprehension łączą instrukcje w jedną atomową transakcję na każde .transact. Błędy wycofują wszystko, a raiseError celowo przerywa transakcję.

Punktów zapisu należy używać do częściowego wycofywania, poziomy izolacji ustawiać w programie, błędy serializacji ponawiać w warstwie efektów, a transakcje utrzymywać krótkie.

Często zadawane pytania

Czy lekcja „Komponowanie transakcji” jest bezpłatna?

Tak — pełny tekst „Komponowanie transakcji” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Scala for Backend Engineering & Functional Programming, przejdź na CoddyKit PRO. Kurs Scala for Backend Engineering & Functional Programming zawiera 4 lekcji w sumie.

Co nauczysz się w „Komponowanie transakcji”?

Bezpiecznie połączy Pan/Pani efekty. Ćwiczysz Scala for Backend Engineering & Functional Programming z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Scala for Backend Engineering & Functional Programming?

Nie wymagamy żadnego doświadczenia. Scala for Backend Engineering & Functional Programming w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Komponowanie transakcji”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Scala for Backend Engineering & Functional Programming?

Tak. Każda lekcja Scala for Backend Engineering & Functional Programming zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Łączenie za pomocą transaktora
  2. Wykonywanie zapytań
  3. Wstawianie i aktualizowanie
  4. Komponowanie transakcji
← Powrót do Scala for Backend Engineering & Functional Programming