Составить необязательные запросы для понимания в doobie?

Я хотел бы выполнить несколько запросов в одной транзакции, используя для понимания в doobie. Что-то вроде:

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 (и кошек), поэтому я не так хорошо знаком с FreeMonads. Я пробовал разные решения, но для проработки для понимания, похоже, что оба блока должны возвращать cats.free.Free[doobie.free.connection.ConnectionOp,?].

Если это правда, есть ли способ преобразовать мой ConnectionIO[Image] (из вызова addImage) в cats.free.Free[doobie.free.connection.ConnectionOp,Option[Image]]?

2 ответа

Решение

Для вашего прямого вопроса, ConnectionIO определяется как type ConnectionIO[A] = Free[ConnectionOp, A]два типа эквивалентны (преобразование не требуется).

Ваша проблема в другом, и ее легко увидеть, если мы шаг за шагом пройдемся по коду. Для простоты я буду использовать Option где ты использовал Optional,

  1. imagePath.map { p => addImage(p) }:

    imagePath это вариант, и map использует A => B преобразовать Option[A] в Option[B],

    поскольку addImage возвращает ConnectionIO[Image]Теперь у нас есть Option[ConnectionIO[Image]]то есть это Option программа, а не ConnectionIO программа.

    Вместо этого мы можем вернуть ConnectionIO[Option[Image]] заменив map с traverse, который использует Traverse typeclass, см. 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], результат optImage.map(_.id) является Option[Option[Int]], не Option[Int] который ожидает ваш метод.

    Один из способов решения этой проблемы (если она соответствует вашим требованиям) - изменить эту часть кода на

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

    flatMap может "присоединиться" к Option с другим, созданным по его значению (если оно существует).

(примечание: нужно добавить import cats.implicits._ чтобы получить синтаксис для traverse).

В общем, некоторые идеи здесь о Traverse, flatMapи т. д. полезны для изучения, и для этого есть две книги: "Scala With Cats" ( https://underscore.io/books/scala-with-cats/) и "Функциональное программирование с помощью Scala" ( https://www.manning.com/books/functional-programming-in-scala)

Автор doobie также недавно выступил с докладом об "эффектах", которые могут быть полезны для улучшения вашей интуиции о таких типах, как Option, IOи т.д.: 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._ и / или cats.syntax.traverse._

Другие вопросы по тегам