Hey folks. I'm a bit unsure about how to handle (doobie) database transactions in a FT style. Say I have a three small algebras and something using them
trait Users[F[_]] { ... }
trait Groups[F[_]] { ... }
trait Mails[F[_]] { ... }
def use[F[_]](implicit U: Users[F], G: Groups[F], M: Mails[F]) = for {
_ <- U.doSomething
_ <- G.doSomething
_ <- M.doSomething
} yield ()
Users and Groups do something with a database while Mail sends mails. Currently, all of their implementations are in IO
and at some point I will call use[IO]
and run that.
That means that Users.doSomething
will take a doobie ConnectionIO
and transact it to get an IO
. This works fine, but how do I handle the case where doSomething
of Users and doSomething
of Groups should happen within the same transaction? There are a few solutions I can think of:
I could change all the implementations to use ConnectionIO
, using LiftIO
to do mail sending. Somewhere in my program I can then transact and run the result of use[ConnectionIO]
.
Provided M.doSomething
is not too time consuming, is there something wrong with changing every implementation from IO
to ConnectionIO
?
I could provide a new algebra trait Transactor[F[_]] { def trans[A](c: ConnectionIO[A]): F[A] }
, implement Users
and Groups
in Terms of ConnectionIO
and change my use
Function:
def use[F[_]](implicit U: Users[F], G: Groups[F], M: Mails[F], T: Transactor[F]) = for {
_ <- T.trans(U.doSomething *> G.doSomething)
_ <- M.doSomething
} yield ()