我最近开始在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对数据库进行多次调用的方法是否有任何缺点?
答案 0 :(得分:2)
当你说for
理解可以减少嵌套时,你是对的。
要解决双重未来问题,请考虑:
existsWithEmail(email).zip(existsWithMobile(mobile)) map {
case (false, false) => // create user
case _ => // already exists
}
如果你有很多这些,你可以使用Future.sequence( Seq(future1, future2, ...) )
将一系列期货变成未来的序列。
您可能希望查看比DAO更多功能的数据库访问惯用语,例如Slick或Anorm。通常这些组合会更好,最终比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
。您可以使用堆栈跟踪记录确切的异常,以帮助调试。