Как правильно использовать IO и OptionT на служебном уровне для понимания? - PullRequest
0 голосов
/ 18 декабря 2018

У меня есть простой интерфейс репозитория с операциями CRUD (возможно, плохая идея передавать неявный сеанс в качестве параметра в общем признаке):

trait Repository[Entity, PK] {
  def find(pk: PK)(implicit session: DBSession): OptionT[IO, Entity]

  def insert(e: Entity)(implicit session: DBSession): IO[Entity]

  def update(e: Entity)(implicit session: DBSession): IO[Entity]

  def delete(pk: PK)(implicit session: DBSession): IO[Int]

  def findAll()(implicit session: DBSession): IO[List[Entity]]
}

И я хочу использовать его следующим образом:

for {
  _ <- repository.insert(???)
  _ <- repository.delete(???)
  v <- repository.find(???).value
  _ <- someFunctionReliesOnReturnedValue(v)
} yield (???)

Кроме того, я хочу остановить выполнение, если v равно None, и откатить транзакцию, если есть какая-либо ошибка (я использую scalikejdbc).Итак, как мне кажется, я должен сделать это на своем уровне обслуживания следующим образом (+ обернуть его в Try или что-то вроде этого, чтобы создать бизнес-исключение):

def logic(???) = {
  DB localTx {
    implicit session => {
      (for {
        _ <- repository.insert(???)
        _ <- repository.delete(???)
        v <- repository.find(???).value
        _ <- someFunctionReliesOnReturnedValue(v)
      } yield (???)).unsafeRunSync() // to rollback transaction if there is any error
    }
  }
}

Проблема здесь: someFunctionReliesOnReturnedValue(v),Это может быть произвольная функция, которая принимает Entity, а не Option[Entity].Как я могу преобразовать результат OptionT[IO, Entity] в IO[Entity] и сохранить семантику Option[]?Это правильный подход или я где-то ошибся?


import java.nio.file.{Files, Paths}

import cats.data.OptionT
import cats.effect.IO
import scalikejdbc._

import scala.util.Try

case class Entity(id: Long, value: String)

object Entity extends SQLSyntaxSupport[Entity] {
  override def tableName: String = "entity"

  override def columnNames: Seq[String] = Seq("id", "value")

  def apply(g: SyntaxProvider[Entity])(rs: WrappedResultSet): Entity = apply(g.resultName)(rs)

  def apply(r: ResultName[Entity])(rs: WrappedResultSet): Entity =
    Entity(rs.long(r.id), rs.string(r.value))
}

trait Repository[Entity, PK] {
  def find(pk: PK)(implicit session: DBSession): OptionT[IO, Entity]

  def insert(e: Entity)(implicit session: DBSession): IO[Entity]
}

class EntityRepository extends Repository[Entity, Long] {
  private val alias = Entity.syntax("entity")

  override def find(pk: Long)(implicit session: DBSession): OptionT[IO, Entity] = OptionT{
    IO{
      withSQL {
        select(alias.resultAll).from(Entity as alias).where.eq(Entity.column.id, pk)
      }.map(Entity(alias.resultName)(_)).single().apply()
    }
  }

  override def insert(e: Entity)(implicit session: DBSession): IO[Entity] = IO{
    withSQL {
      insertInto(Entity).namedValues(
        Entity.column.id -> e.id,
        Entity.column.value -> e.value,
      )
    }.update().apply()
    e
  }
}

object EntityRepository {
  def apply(): EntityRepository = new EntityRepository()
}

object Util {
  def createFile(value: String): IO[Unit] = IO(Files.createDirectory(Paths.get("path", value)))
}

class Service {
  val repository = EntityRepository()

  def logic(): Either[Throwable, Unit] = Try {
    DB localTx {
      implicit session => {
        val result: IO[Unit] = for {
          _ <- repository.insert(Entity(1, "1"))
          _ <- repository.insert(Entity(2, "2"))
          e <- repository.find(3)
          _ <- Util.createFile(e.value) // error
          //after this step there is possible more steps (another insert or find)
        } yield ()
        result.unsafeRunSync()
      }
    }
  }.toEither
}

object Test extends App {
  ConnectionPool.singleton("jdbc:postgresql://localhost:5433/postgres", "postgres", "")
  val service = new Service()
  service.logic()
}

Таблица:

create table entity (id numeric(38), value varchar(255));

И я получил ошибку компиляции:

Ошибка: (69, 13) несоответствие типов;найдено: cats.effect.IO [Блок] требуется: cats.data.OptionT [cats.effect.IO ,?] _ <- Util.createFile (e.value) </p>

1 Ответ

0 голосов
/ 18 декабря 2018

В общем, вы должны преобразовать все ваши разные результаты в ваш «самый общий» тип, имеющий монаду.В этом случае это означает, что вы должны использовать OptionT[IO, A] на протяжении всего вашего понимания, конвертируя все эти IO[Entity] в OptionT[IO, Entity] с OptionT.liftF:

for {
  _ <- OptionT.liftF(repository.insert(???))
  _ <- OptionT.liftF(repository.delete(???))
  v <- repository.find(???)
  _ <- someFunctionReliesOnReturnedValue(v)
} yield (???)

Если у вас было Option[A]Вы могли бы использовать OptionT.fromOption[IO].Проблемы возникают из-за попытки смешать монады в одном и том же для понимания.

Это уже остановит выполнение, если любой из них приведет к None.Что касается отката транзакции, это зависит от того, как работает ваша библиотека взаимодействия с БД, но если она обрабатывает исключения путем отката, тогда да, unsafeRunSync будет работать.Если вы также хотите откатить его, выдав исключение, когда результат равен None, вы можете сделать что-то вроде:

val result: OptionT[IO, ...] = ...
result.value.unsafeRunSync().getOrElse(throw new FooException(...))
...