Scala 中的事务性方法 Play with Slick(类似于 Spring @Transactional,也许吧?)

Transactional method in Scala Play with Slick (similar to Spring @Transactional, maybe?)

我知道 scala 作为一种函数式语言,其工作方式应该不同于常见的 OO 语言,例如 Java,但我确信必须有一种方法来包装一组数据库在单个事务中更改,确保原子性以及所有其他 ACID 属性.

如 slick 文档 (http://slick.lightbend.com/doc/3.1.0/dbio.html) 中所述,DBIOAction 允许在事务中对数据库操作进行分组,如下所示:

val a = (for {
  ns <- coffees.filter(_.name.startsWith("ESPRESSO")).map(_.name).result
  _ <- DBIO.seq(ns.map(n => coffees.filter(_.name === n).delete): _*)
} yield ()).transactionally

val f: Future[Unit] = db.run(a)

然而,我的用例(以及我能想到的大多数真实世界的例子),我有一个带有控制器的代码结构,它公开了我的 REST 端点的代码,该控制器调用多个服务,每个服务将委托DAO 的数据库操作。

我常用代码结构的粗略示例:

class UserController @Inject(userService: UserService) {
  def register(userData: UserData) = {
    userService.save(userData).map(result => Ok(result))
  }
}

class UserService @Inject(userDao: UserDao, addressDao: AddressDao) {
  def save(userData: UserData) = {
    for {
      savedUser <- userDao.save(userData.toUser)
      savedAddress <- addressDao.save(userData.addressData.toAddress)
    } yield savedUser.copy(address = savedAddress)
  }
}

class SlickUserDao {
  def save(user: User) = {
    db.run((UserSchema.users returning UserSchema.users)).insertOrUpdate(user)
  }
}

这是一个简单的例子,但大多数在服务层都有更复杂的业务逻辑。

我不想:

  1. 我的 DAO 具有业务逻辑并决定对哪些数据库操作 运行。
  2. Return 来自我的 DAO 的 DBAction 并公开持久性 类。这完全违背了最初使用 DAO 的目的,并使进一步的重构变得更加困难。

但我绝对希望围绕我的整个 Controller 进行事务处理,以确保如果任何代码失败,将回滚执行该方法时所做的所有更改。

如何在 Scala Play 应用程序中使用 Slick 实现完整的控制器事务性?我似乎找不到有关如何执行此操作的任何文档。

此外,如何在 slick 中禁用自动提交?我确定有办法,我只是错过了一些东西。

编辑:

所以多读一点,我觉得现在我更好地理解了 slick 如何使用与数据库和会话的连接。这很有帮助:http://tastefulcode.com/2015/03/19/modern-database-access-scala-slick/

我正在做的是在 futures 中组合的情况,根据这篇文章,没有办法使用相同的连接和会话来进行此类多次操作。

问题是:我真的不能用任何其他类型的构图。我有相当多的业务逻辑需要在查询之间执行。

我想我可以更改我的代码以允许我使用动作组合,但正如我之前提到的,这迫使我在编写业务逻辑时考虑事务性等方面。那不应该发生。它污染了业务代码并使编写测试变得更加困难。

这个问题有解决办法吗?有没有 git 项目可以解决我错过的这个问题?或者,更激烈的是,任何其他支持此的持久性框架?根据我的阅读,Anorm 对此提供了很好的支持,但我可能误解了它并且不想更改框架以发现它不支持(就像 Slick 发生的那样)。

slick 中没有事务注释之类的东西。你的第二个 "do not want" 实际上是要走的路。从你的 DAO 中 return DBIO[User] 是完全合理的,这根本不会违背他们的目的。这就是光滑的工作方式。

class UserController @Inject(userService: UserService) {
  def register(userData: UserData) = {
    userService.save(userData).map(result => Ok(result))
  }
}

class UserService @Inject(userDao: UserDao, addressDao: AddressDao) {
  def save(userData: UserData): Future[User] = {
    val action = (for {
      savedUser <- userDao.save(userData.toUser)
      savedAddress <- addressDao.save(userData.addressData.toAddress)
      whatever <- DBIO.successful(nonDbStuff)
    } yield (savedUser, savedAddress)).transactionally

    db.run(action).map(result => result._1.copy(result._2))
  }
}

class SlickUserDao {
  def save(user: User): DBIO[User] = {
    (UserSchema.users returning UserSchema.users).insertOrUpdate(user)
  }
}
  • 您的服务 class 中 save 的签名仍然相同。
  • 控制器中没有数据库相关的东西。
  • 您可以完全控制交易。
  • 与您的原始示例相比,我找不到上面的代码更难维护/重构的情况。

还有一个非常详尽的讨论,您可能会感兴趣。参见 Slick 3.0 withTransaction blocks are required to interact with libraries