17

Doobie book says that it's a good practice to return ConnectionIO from your repository layer. It gives an ability to chain calls and perform them in one transaction. Nice and clear.

Now let's imagine we are working on REST API service and our scenario is:

  1. Find an object in database
  2. Perform some async manipulation (using cats.effect.IO or monix.eval.Task) with this object.
  3. Store the object in database.

And we want to perform all these steps inside 1 transaction. The problem is that without natural transformation which is given for us by transactor.trans() we are working inside 2 monads - Task and ConnectionIO. That's not possible.

The question is - how to mix doobie ConnectionIO with any effect monad in 1 composition such as we are working in 1 transaction and able to commit/rollback all DB mutations at the end of the world?

Thank you!

UPD: small example

def getObject: ConnectionIO[Request]                      = ???
def saveObject(obj: Request): ConnectionIO[Request]       = ???
def processObject(obj: Request): monix.eval.Task[Request] = ???

val transaction:??? = for {
    obj       <- getObject             //ConnectionIO[Request]
    processed <- processObject(obj)    //monix.eval.Task[Request]
    updated   <- saveObject(processed) //ConnectionIO[Request]
  } yield updated

UPD2: The correct answer provided by @oleg-pyzhcov is to lift your effect datatypes to ConnectionIO like this:

def getObject: ConnectionIO[Request]                      = ???
def saveObject(obj: Request): ConnectionIO[Request]       = ???
def processObject(obj: Request): monix.eval.Task[Request] = ???

val transaction: ConnectionIO[Request] = for {
    obj       <- getObject                                           //ConnectionIO[Request]
    processed <- Async[ConnectionIO].liftIO(processObject(obj).toIO) //ConnectionIO[Request]
    updated   <- saveObject(processed)                               //ConnectionIO[Request]
} yield updated
val result: Task[Request] = transaction.transact(xa)
Eugene Zhulkov
  • 505
  • 3
  • 13
  • Could you give some simple example code? I think this shouldn't be a problem if you compose your ConnectionIO values within the Task Monad, but it'd help to see a clearer use-case. – Rajit May 22 '18 at 17:36

1 Answers1

19

ConnectionIO in doobie has a cats.effect.Async instance, which, among other things, allows you do turn any cats.effect.IO into ConnectionIO by means of liftIO method:

import doobie.free.connection._
import cats.effect.{IO, Async}
val catsIO: IO[String] = ???
val cio: ConnectionIO[String] = Async[ConnectionIO].liftIO(catsIO)

For monix.eval.Task, your best bet is using Task#toIO and performing the same trick, but you'd need a monix Scheduler in scope.

Oleg Pyzhcov
  • 7,323
  • 1
  • 18
  • 30
  • Thank you! Didn't think that I can lift `Async` to `ConnectionIO` :) Talking of Monix Task you are able to lift it to any `Async` instance using `Task.to[Async]`. In this case the answer would be: `val monixTask:Task[String] = ??? val cio: ConnectionIO[String] = monixTask.to[ConnectionIO]` – Eugene Zhulkov May 23 '18 at 08:37
  • 3
    `.to[ConnectionIO]` appears to work for `cats.effect.IO` now – Lasf Dec 18 '18 at 03:25
  • It seems there is no `Async[ConnectionIO]` in ce3. – Some Name Jul 29 '22 at 12:18
  • There's no longer Async instance for ConnectionIO as of CE3/Doobie 1.x. See https://stackoverflow.com/a/71257623 for a solution that uses WeakAsync – Jacob Wang Oct 07 '22 at 14:01