Составить необязательные запросы для понимания в 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
,
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
из стандартной библиотеки, но в более общем смысле.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._