如何在for推导式中实现服务调用的撤销/补偿机制?
使用Cats优化带有补偿回滚的Scala服务调用逻辑
这是一个典型的资源获取-使用-清理/回滚场景,正好可以用Cats提供的工具优雅解决,既保持代码简洁性,又能清晰处理补偿逻辑。先明确你的核心需求:
- 先执行
barName()这个GET调用 - 调用
insertFoo成功后,依次执行四个子插入操作 - 若子插入任意一步失败,必须调用
deleteFoo回滚已插入的Foo
优化后的实现方案
我们可以用Cats的guaranteeCase方法(属于MonadError的语法),它能精准区分业务逻辑的成功/失败状态,从而触发对应的回滚操作。以下是优化后的代码:
import cats.MonadError import cats.syntax.all._ // 假设你的Foo类型定义如下(根据实际项目调整) case class Foo(id: Option[String]) def process[F[_]: MonadError[*[_], Throwable]]: F[Foo] = for { // 第一步:执行GET调用获取barName barName <- barName() // 第二步:插入Foo,获取结果 foo <- insertFoo(barName) // 第三步:根据foo.id是否存在,处理子插入和回滚逻辑 result <- foo.id match { case Some(fooId) => // 执行所有子插入操作,用*>串联忽略中间结果 val insertChildren = insertChild1(fooId) *> insertChild2(fooId) *> insertChild3(fooId) *> insertChild4(fooId) *> MonadError[F, Throwable].pure(foo) // 使用guaranteeCase处理成功/失败分支 insertChildren.guaranteeCase { // 子插入失败时,执行回滚操作 case Left(_) => deleteFoo(fooId) // 子插入成功时,不做任何操作 case Right(_) => MonadError[F, Throwable].unit } case None => // 根据业务需求处理:如果Foo.id不存在,是直接返回还是抛错? // 示例1:直接返回foo MonadError[F, Throwable].pure(foo) // 示例2:如果业务要求Foo必须有id,抛出错误 // MonadError[F, Throwable].raiseError(new IllegalArgumentException("Inserted Foo has no valid id")) } } yield result
方案优势
- 逻辑清晰解耦:回滚逻辑和业务插入逻辑分开,通过
guaranteeCase明确绑定,一眼就能看懂失败时的补偿行为 - 保持函数式风格:用for推导式和Cats的语法糖(
*>)保持代码流畅性,没有冗余的嵌套flatMap - 类型安全:借助
MonadError的约束,确保所有操作都在同一个效果类型F[_]下,错误处理一致 - 容错可靠:不管子插入是抛出异常还是返回失败类型,
guaranteeCase都会捕获到失败状态,触发回滚
补充说明
- 确保
deleteFoo的返回类型是F[Unit],和其他操作保持一致的效果类型 - 如果你的项目使用自定义错误类型(而非
Throwable),只需把MonadError[*[_], Throwable]替换为MonadError[*[_], YourErrorType]即可 *>是Cats的语法糖,等价于flatMap(_ => nextOperation),用于串联不需要保留中间结果的操作,让代码更简洁
内容的提问来源于stack exchange,提问作者jen
相关产品推荐
相关产品推荐

