Jika Anda bekerja dengan satu basis data yang mendukung transaksi, Anda bahkan tidak memikirkan konsistensi - basis data itu melakukan segalanya untuk Anda. Jika Anda memiliki beberapa database, sistem terdistribusi, atau bahkan, misalnya, MongoDB hingga versi 4, semuanya tidak begitu cerah.
Pertimbangkan contoh - kami ingin menyimpan file di repositori dan menambahkan tautan ke dalamnya dalam dua dokumen. Tentu saja kami menginginkan atomicity - file disimpan dan ditambahkan ke dokumen, atau tidak (efek kucing IO digunakan selanjutnya):
saveDataToFile(data)
.flatMap { file =>
addFileRef(documentId, file)
.flatMap { result =>
addFileRef(fileRegistry, file)
.flatMap { result =>
???
}
.handleErrorWith { error =>
removeFileRef(documentId, file).attempt >> IO.raiseError(error)
}
}
.handleErrorWith { error =>
removeFile(file).attempt >> IO.raiseError(error)
}
}
? Pyramid of doom.
! , .
, , . ( ) "" .
- Saga, . / , . , . — Scala .
— :
final case class Action(
perform: ???,
commit: ???,
compensate: ???
)
? - () => Try[T]
, IO
— , cats.
final case class Action(
perform: IO[Unit],
commit: IO[Unit],
compensate: IO[Unit]
)
commit
perform
, compensate
perform
?
:
final case class Action[T](
perform: IO[T],
commit: T => IO[Unit],
compensate: T => IO[Unit]
)
, T
— .. perform
.
:
def saveDataToFile(data: Data): IO[File] = ???
def removeFile(file: File): IO[Unit] = ???
def saveDataAction(data: Data): Action[File] = Action(
perform = saveDataToFile(data),
compensate = removeFile
)
—
, , ? Seq[Action[_]]
— . — , .
— - ? .
:
final case class ActionChain[A, B](
first: Action[A],
next: A => Action[B]
)
, — . :
sealed trait Transaction[T]
final case class Action[T](...) extends Transaction[T]
final case class ActionChain[A, B](
first: Transaction[A],
next: A => Transaction[B]
) extends Transaction[B]
! , :
ActionChain(
saveDataAction(data),
{ file =>
ActionChain(
addFileRefAction(documentId, file),
{ _ =>
addFileRefAction(fileRegistry, file)
}
)
}
)
, .
, — , ?
, , IO
.
, . "" — .
Transaction
case
' . , :
- —
private def compile[R](restOfTransaction: T => IO[R]): IO[R] = this match {
case Action(perform, commit, compensate) => perform.flatMap { t =>
restOfTransaction(t).redeemWith(
bind = commit(t).attempt >> IO.pure(_),
recover = compensate(t).attempt >> IO.raiseError(_)
)
}
case ActionChain(first, next) => ???
}
, CatsredeemWith
, attempt
/ ( , ), >>
" ", IO.pure
IO.raiseError
continue(t)
— .
— , :
private def compile[R](restOfTransaction: T => IO[R]): IO[R] = this match {
case Action(perform, commit, compensate) => ...
case ActionChain(first, next) =>
first.compile { a =>
next(a).compile { t =>
restOfTransaction(t)
}
}
}
, :
sealed trait Transaction[T] {
def compile: IO[T] = compile(IO.pure)
}
IO
, commit
/compensate
, ( / IO
, ).
, . , ? , .
, . :
-, Action.compile(restOfTransaction)
:
perform
, restOfTransaction
( T
, perform
)compile
: perform
, restOfTransaction
( T
)perform
, commit
compensate
restOfTransaction
( redeemWith
)
ActionChain.compile(restOfTransaction)
, Action
' , compile
':
transaction.compile(restOfTransaction)
===
action1.compile(t1 =>
action2.compile(t2 =>
action3.compile(t3 => ...
restOfTransaction(tn))))
, ActionChain.first
ActionChain
:
ActionChain(ActionChain(action1, t1 => action2), t2 => action3).compile(restOfTransaction) >>
ActionChain(action1, t1 => action2).compile(t2 => action3.compile(restOfTransaction)) >>
action1.compile(t1 => action2.compile(t2 => action3.compile(restOfTransaction))) []
Action.compile
:
commit
compensate
?
:
- ( )
- ,
- ???
- PROFIT!
, :
ActionChain(
saveDataAction(data),
{ file =>
ActionChain(
addFileRefAction(documentId, file),
{ _ =>
addFileRefAction(fileRegistry, file)
}
)
}
).compile
chain
ActionChain
, :
sealed trait Transaction[T] {
def chain[R](f: T => Transaction[R]): Transaction[R] = ActionChain(this, f)
}
saveDataAction(data).chain { file =>
addFileRefAction(documentId, file).chain { _ =>
addFileRefAction(fileRegistry, file)
}
}.compile
chain
flatMap
, , () !
, Scala, for
cats
, ( ).
, — !
sealed trait Transaction[T] {
def flatMap[R](f: T => Transaction[R]): Transaction[R] = ActionChain(this, f)
def map[R](f: T => R): Transaction[R] = flatMap { t => Action(IO(f(t))) }
}
Scala, :
def saveDataAndAddFileRefs(data: Data, documentId: ID): Transaction[Unit] = for {
file <- saveDataAction(data)
_ <- addFileRefAction(documentId, file)
_ <- addFileRefAction(fileRegistry, file)
} yield ()
saveDataAndAddFileRefs(data, documentId).compile
, — . — , .
Untuk cats
Anda perlu secara eksplisit mendeklarasikan bukti bahwa Transaction
ini adalah monad - turunan dari typclass:
object Transaction {
implicit object MonadInstance extends Monad[Transaction] {
override def pure[A](x: A): Transaction[A] = Action(IO.pure(x))
override def flatMap[A, B](fa: Transaction[A])(f: A => Transaction[B]): Transaction[B] = fa.flatMap(f)
override def tailRecM[A, B](a: A)(f: A => Transaction[Either[A, B]]): Transaction[B] = f(a).flatMap {
case Left(a) => tailRecM(a)(f)
case Right(b) => pure(b)
}
}
}
Apa yang ini berikan pada kita? Kemampuan untuk menggunakan metode yang sudah jadi dari cats
, misalnya:
val transaction = dataChunks.traverse_ { data =>
saveDataAndAddFileRefs(data, documentId)
}
transaction.compile
Semua kode tersedia di sini: https://github.com/atamurius/scala-transactions