#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 => ...
...
}