Scala Slick — определяет, какой DBIO не работает в DBIO.sequence

#scala #slick #scala-cats

#scala #slick #scala-кошки

Вопрос:

Я выполняю транзакционно 3 разные операции с БД следующим образом

 // firstDBIO, secondDBIOA, thirdDBIO: DBIOAction[Unit]

F.delay {
  val unitOfWork = DBIO.sequence(
    List(
      firstDBIO,
      secondDBIO,
      thirdDBIO,
    ),
  )
  db.run(unitOfWork.transactionally)
}.futureLift.void.map(_.asRight[ImportError]).recover {
  case ex: SQLException => Left(ImportError.UnexpectedError)
}
 

Это работает правильно, но, когда транзакция завершается с ошибкой, recover я не могу создать логику, основанную на том, какой из DBIO них вызвал ошибку (я не хочу полагаться на SQLException ).

Я хотел бы иметь возможность сделать что-то вроде

 .recover {
  case ex: ImportError.CauseFirst => ...
  case ex: ImportError.CauseSecond => ...
  case ex: ImportError.CauseThird => ...
  ...
}
 

Ответ №1:

Если вы используете .sequence then, вы просто потерпите неудачу при первом сбое future. У вас есть 2 варианта:

  • сопоставьте каждую ошибку DBIO с числом — я думаю, вы могли бы злоупотреблять .cleanUp методом с помощью чего-то вроде
     dbio.cleanUp({
      case Some(error) => DBIO.failed(improveError(error)) // add idx to Exception or sth
      case None => DBIO.successful(())
    }, keepFailure = false)
     
  • сохраняйте отдельные результаты как Try и разрешайте их после транзакции
     dbio.asTry
    // then use db.run(DBIO.sequence(dbios).transactionally)
    // to get Future[List[Try[Int]]]
     

Я не уверен, как последний будет обрабатывать транзакции и откаты по сравнению с первым, но оба случая позволят вам узнать, какое действие не удалось.

Комментарии:

1. Решение с помощью Try больше не будет прерываться после первого сбоя, что может быть проблемой.

2. Было бы неплохо, если бы в DBIO был fold , mapError или recoverWith , но, согласно их документации, использование cleanUp для преобразования throwable действительно является предполагаемым вариантом использования. Так что здесь нет злоупотреблений 😉

Ответ №2:

Я обнаружил, что это решение — именно то, что я искал

 dbio.asTry.flatMap {
  case Success(v) => DBIO.successful(v)
  case Failure(e) => DBIO.failed(ImportError.CauseFirst)
}
 

Он сохраняет результат успеха / сбоя, вызывающий прерывание атомной транзакции в соответствии с запросом.
В конце концов, это просто отображение ошибки, содержащейся в сбое, действительно, для этого весьма полезно реализовать функцию

 implicit class DBIOOps[A](dbio: DBIO[A]) {
  def mapFailure(f: Throwable => E with Throwable) = dbio.asTry.flatMap {
    case Success(v) => DBIO.successful(v)
    case Failure(e) => DBIO.failed(f(e))
  }
}
 
 F.delay {
  val unitOfWork = DBIO.sequence(
    List(
      firstDBIO.mapFailure(_ => ImportError.CauseFirst),
      secondDBIO.mapFailure(_ => ImportError.CauseSecond),
      thirdDBIO.mapFailure(_ => ImportError.CauseThird),
      ...
    ),
  )
  db.run(unitOfWork.transactionally)
}.futureLift.void.map(_.asRight[ImportError]).recover {
  case ex: ImportError.CauseFirst => ...
  case ex: ImportError.CauseSecond => ...
  case ex: ImportError.CauseThird => ...
  ...
}