如何链接Futures进行异步数据库I / O处理?

时间:2014-12-19 14:55:55

标签: scala playframework-2.0

我最近开始在Play Scala中开发一个应用程序。虽然我已经将Play Java用于了几个应用程序,但我也是Scala和Play Scala的新手。

我使用DAO模式来抽象数据库交互。 DAO包含插入,更新删除的方法。在阅读了异步和线程池相关的文档后,我认为使数据库交互异步非常重要,除非你调整Play默认线程池以包含许多线程。

为了确保异步处理所有数据库调用,我进行了所有调用以直接返回Future而不是值。我为数据库交互创建了一个单独的执行上下文。

trait Dao[K, V] {
  def findById(id: K): Future[Option[V]]
  def update(v: V): Future[Boolean]
  [...]
}

这导致了行为中非常复杂且深度嵌套的代码。

trait UserDao extends Dao[Long, User] {
  def existsWithEmail(email: String): Future[Boolean]
  def insert(u: User) Future[Boolean]
}

object UserController extends Controller {
  def register = Action {
    [...]
    userDao.existsWithEmail(email).flatMap { exists =>
      exits match {

        case true =>
          userDao.insert(new User("foo", "bar")).map { created =>
            created match {
              case true => Ok("Created!")
              case false => BadRequest("Failed creation")
            }
          }

        case false =>
          Future(BadRequest("User exists with same email"))
      }
    }
  }
}

以上是最简单的操作示例。由于涉及更多数据库调用,嵌套级别变得更深。虽然我认为可以通过使用理解来减少一些嵌套,但我怀疑我的方法本身是否从根本上是错误的?

考虑我需要创建用户的情况

一个。如果不存在同一个电子邮件地址。

湾如果不存在同一个手机号码。

我可以创建两个期货,

f(a)检查用户是否存在电子邮件。

f(b)检查用户是否存在移动设备。

我不能去插入新用户,除非我验证两个条件都评估为false。我实际上可以让f(a)和f(b)并行运行。在f(a)评估为真的情况下,并行执行可能是不合需要的,否则可能有利。创建用户的第3步取决于这两个未来,所以我想知道跟随是否同样好?

trait UserDao extends Dao[Long, User] {
  def existsWithEmail(email: String): Boolean
  def existsWithMobile(mobile: String): Boolean
  def insert(u: User): Unit
}

def register = Action {
  implicit val dbExecutionContext = myconcurrent.Context.dbExceutionContext

  Future {
    if (!userDao.existsWithEmail(email) && !userDao.existsWithMobile(mobile) {
      userDao.insert(new User("foo", "bar")
      Ok("Created!")
    } else {
      BadRequest("Already exists!")
    }
  }
}

哪一个是更好的方法?使用单个Future对数据库进行多次调用的方法是否有任何缺点?

2 个答案:

答案 0 :(得分:2)

当你说for理解可以减少嵌套时,你是对的。

要解决双重未来问题,请考虑:

existsWithEmail(email).zip(existsWithMobile(mobile)) map {
  case (false, false) => // create user
  case _              => // already exists
}

如果你有很多这些,你可以使用Future.sequence( Seq(future1, future2, ...) )将一系列期货变成未来的序列。

您可能希望查看比DAO更多功能的数据库访问惯用语,例如SlickAnorm。通常这些组合会更好,最终比DAO更灵活。

附注:使用if/else进行简单的真/假测试比使用match/case更有效,并且是首选样式。

答案 1 :(得分:1)

我在scala中使用for理解解决了这个问题。我添加了一些隐式类型转换器来帮助处理错误。

最初我做了类似的事情,

def someAction = Action.async {
  val result = 
    for {
      student <- studentDao.findById(studentId)
      if (student.isDefined)
      parent <- parentDao.findById(student.get.parentId)
      if (parent.isDefined)
      address <- addressDao.findById(parent.get.addressId)
      if (address.isDefined)
    } yield {
      // business logic
    }

  result fallbackTo Future.successful(BadRequest("Something went wrong"))
}

这是代码最初的结构,以对抗期货之间的依赖关系。请注意,每个后续的未来取决于以前的未来。此外,每个findById都会返回Future[Option[T]],因此需要if for才能处理方法返回None的情况。如果任何期货评估为fallbackTo,我会在Future上使用BadRequest方法回退到None结果(如果任何条件因为理解而失败,则返回失败的未来)上述方法的另一个问题是它会抑制遇到的任何类型的异常(即使是像NPE一样微不足道的异常),而只是回退到BadRequest,这非常糟糕。

上述方法能够对抗期权的未来并处理失败的案例,尽管确切地知道理解中的哪些期货失败是没有帮助的。为了克服这个限制,我使用了隐式类型转换器。

object FutureUtils {
  class FutureProcessingException(msg: String) extends Exception(msg)
  class MissingOptionValueException(msg: String) extends FutureProcessingException(msg)

  protected final class OptionFutureToOptionValueFuture[T](f: Future[Option[T]]) {
    def whenUndefined(error: String)(implicit context: ExecutionContext): Future[T] = {
      f.map { value =>
        if (value.isDefined) value.get else throw new MissingOptionValueException(error)
      }
    }
  }

  import scala.language.implicitConversions

  implicit def optionFutureToValueFutureConverter[T](f: Future[Option[T]]) = new OptionFutureToOptionValueFuture(f)

}

上面的隐含转换使我可以编写可读的链接多个未来的理解。

import FutureUtils._

def someAction = Action.async {
  val result = 
    for {
      student <- studentDao.findById(studentId) whenUndefined "Invalid student id"
      parent <- parentDao.findById(student.get.parentId) whenUndefined "Invalid parent id"
      address <- addressDao.findById(parent.get.addressId) whenUndefined "Invalid address id"
    } yield {
      // business logic
    }

  result.recover {
    case fpe: FutureProcessingException => BadRequest(fpe.getMessage)
    case t: Throwable => InternalServerError
  } 
}

上述方法确保由缺少Option值导致的所有异常都作为BadRequest处理,并具有关于确切失败的具体消息。所有其他失败都被视为InternalServerError。您可以使用堆栈跟踪记录确切的异常,以帮助调试。