Doobie and DB access composition within 1 transaction
Asked Answered
P

1

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)
Painty answered 22/5, 2018 at 16:55 Comment(1)
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.Godmother
H
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.

Hijoung answered 23/5, 2018 at 6:26 Comment(4)
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]Painty
.to[ConnectionIO] appears to work for cats.effect.IO nowProbability
It seems there is no Async[ConnectionIO] in ce3.Curr
There's no longer Async instance for ConnectionIO as of CE3/Doobie 1.x. See stackoverflow.com/a/71257623 for a solution that uses WeakAsyncLiquidize

© 2022 - 2024 — McMap. All rights reserved.