在 doobie 中为 for-comprehension 编写可选查询?

Compose optional queries for for-comprehension in doobie?

我想 运行 在一个事务中使用 doobie 中的 for-comprehension 进行多个查询。类似于:

def addImage(path:String) : ConnectionIO[Image] = {
  sql"INSERT INTO images(path) VALUES($path)".update.withUniqueGeneratedKeys('id', 'path')
}

def addUser(username: String, imageId: Optional[Int]) : ConnectionIO[User] = {
  sql"INSERT INTO users(username, image_id) VALUES($username, $imageId)".update.withUniqueGeneratedKeys('id', 'username', 'image_id')
}

def createUser(username: String, imagePath: Optional[String]) : Future[User] = {
  val composedIO : ConnectionIO[User] = for {
    optImage <- imagePath.map { p => addImage(p) }
    user <- addUser(username, optImage.map(_.id))
  } yield user

  composedIO.transact(xa).unsafeToFuture
}

我刚开始使用 doobie(和 cats),所以我对 FreeMonads 不是很熟悉。我一直在尝试不同的解决方案,但为了理解工作,看起来两个块都需要 return a cats.free.Free[doobie.free.connection.ConnectionOp,?].

如果这是真的,有没有办法将我的 ConnectionIO[Image](从 addImage 调用)转换为 cats.free.Free[doobie.free.connection.ConnectionOp,Option[Image]]?

对于你直接的问题,ConnectionIO定义为type ConnectionIO[A] = Free[ConnectionOp, A],即两种类型是等价的(不需要转换)。

你的问题不一样,我们一步步看代码就很容易看出来。为简单起见,我将在您使用 Optional.

的地方使用 Option
  1. imagePath.map { p => addImage(p) }:

    imagePath 是一个 Option,map 使用 A => BOption[A] 转换为 Option[B]

    由于 addImage return 是 ConnectionIO[Image],我们现在有一个 Option[ConnectionIO[Image]],即这是一个 Option 程序,而不是 ConnectionIO 程序。

    我们可以通过将 map 替换为 traverse 来代替 return 一个 ConnectionIO[Option[Image]],它使用 Traverse 类型类,参见 https://typelevel.org/cats/typeclasses/traverse.html有关其工作原理的一些详细信息。但是一个基本的直觉是 map 会给你一个 F[G[B]]traverse 却给你一个 G[F[B]]。从某种意义上说,它的工作方式类似于标准库中的 Future.traverse,但方式更通用。

  2. addUser(username, optImage.map(_.id))

    这里的问题是给定 optImage 是一个 Option[Image],它的 id 字段是一个 Option[Int],[=39= 的结果] 是 Option[Option[Int]],而不是您的方法所期望的 Option[Int]

    解决此问题的一种方法(如果符合您的要求)是将这部分代码更改为

    addUser(username, optImage.flatMap(_.id))

    flatMap 可以 "join" 一个 Option 和另一个由它的值创建的(如果它存在的话)。

(注意:您需要添加 import cats.implicits._ 以获得 traverse 的语法)。

总的来说,这里关于TraverseflatMap等的一些想法,对学习很有帮助,有两本书是"Scala With Cats"(https://underscore.io/books/scala-with-cats/) and "Functional Programming with Scala" (https://www.manning.com/books/functional-programming-in-scala)

doobie 的作者最近也发表了一篇关于 "effects" 的演讲,这可能有助于提高您对 OptionIO 等类型的直觉:https://www.youtube.com/watch?v=po3wmq4S15A

如果我理解你的意图,你应该使用 traverse 而不是 map:

  val composedIO : ConnectionIO[User] = for {
    optImage <- imagePath.traverse { p => addImage(p) }
    user <- addUser(username, optImage.map(_.id))
  } yield user

您可能需要导入 cats.instances.option._ and/or cats.syntax.traverse._