如何重构以在case语句返回不同类型并执行Doobie查询
Doobie中根据条件执行不同查询且返回不同Either Right类型的实现方案
问题描述
需要根据匹配条件执行不同的Doobie查询,这些查询返回的Either的Right类型可能不同(比如有的返回SomeType1,有的返回SomeType2)。原代码尝试在EitherT.fromEither的match分支中返回ConnectionIO[Either[String, _]],但存在类型不匹配的问题,且不在意函数最终返回类型(当前为List[User],可改为Unit或其他类型)。
原代码示例:
def blah(locationId: Int): ConnectionIO[Either[String, List[User]]] = (for { usersToUpdate <- EitherT.fromEither[ConnectionIO] ( UserDao.getUsersByLocation(locationId) match { case Nil => Left("...") case first :: _ => SomeDao.blah2(..) // ConnectionIO[Either[String, SomeType1]] case ... => SomeDao.blah(..) // ConnectionIO[Either[String, SomeType2]] } } ) updatedUsers <- usersToUpdate.traverse { EitherT(_) } } yield updatedUsers).value
问题分析
原代码存在两个核心问题:
EitherT.fromEither接收的是纯Either值,但分支中返回的是ConnectionIO[Either[String, _]],类型不兼容;- 不同match分支的
EitherRight类型不同(SomeType1/SomeType2),无法自动统一为同一类型,导致编译失败。
可行实现方案
方案1:统一返回为Unit(推荐,符合“不在意返回类型”的需求)
既然不在乎最终返回类型,可以将所有分支的Right结果都映射为Unit,统一返回类型为ConnectionIO[Either[String, Unit]]。同时需要先将UserDao.getUsersByLocation的结果lift到EitherT上下文,再进行匹配:
def blah(locationId: Int): ConnectionIO[Either[String, Unit]] = (for { // 先获取用户列表,lift到EitherT上下文 users <- EitherT.liftF(UserDao.getUsersByLocation(locationId)) result <- users match { case Nil => // 返回左值,指定Right类型为Unit EitherT.leftT[ConnectionIO, Unit]("No users found for the location") case first :: _ => // 执行blah2,将Right的SomeType1映射为Unit EitherT(SomeDao.blah2(...).map(_.map(_ => ()))) case otherCase => // 执行blah,将Right的SomeType2映射为Unit EitherT(SomeDao.blah(...).map(_.map(_ => ()))) } } yield result).value
方案2:定义公共父类型保留类型信息
如果需要保留不同分支的结果类型信息,可以定义一个密封特质作为所有结果类型的父类,将不同分支的结果包装为该特质的子类,从而统一类型:
// 定义公共父类型 sealed trait QueryResult case class ResultType1(value: SomeType1) extends QueryResult case class ResultType2(value: SomeType2) extends QueryResult def blah(locationId: Int): ConnectionIO[Either[String, QueryResult]] = (for { users <- EitherT.liftF(UserDao.getUsersByLocation(locationId)) result <- users match { case Nil => EitherT.leftT[ConnectionIO, QueryResult]("No users found for the location") case first :: _ => // 将SomeType1包装为ResultType1 EitherT(SomeDao.blah2(...).map(_.map(ResultType1(_)))) case otherCase => // 将SomeType2包装为ResultType2 EitherT(SomeDao.blah(...).map(_.map(ResultType2(_)))) } } yield result).value
方案3:使用Any(不推荐,丢失类型安全)
如果完全不在意类型安全,可以直接将Right类型统一为Any,但这种方式会失去Scala的类型检查优势,仅作为临时应急方案:
def blah(locationId: Int): ConnectionIO[Either[String, Any]] = (for { users <- EitherT.liftF(UserDao.getUsersByLocation(locationId)) result <- users match { case Nil => EitherT.leftT[ConnectionIO, Any]("No users found for the location") case first :: _ => EitherT(SomeDao.blah2(...).map(_.asInstanceOf[Either[String, Any]])) case otherCase => EitherT(SomeDao.blah(...).map(_.asInstanceOf[Either[String, Any]])) } } yield result).value
内容的提问来源于stack exchange,提问作者Blankman
相关产品推荐
相关产品推荐

