Slick 3:如何使用事务实现存储库模式?

Slick 3: How to implement repository pattern with transactions?

在我的 play framework (2.5) 应用程序中,我需要为服务编写单元测试。

我需要隔离数据访问逻辑才能单独测试服务层, 为此,我想创建存储库接口并在我的单元测试中模拟它们:

class UserService {
   def signUpNewUser(username: String, memberName: String): Future[Unit] {
      val userId = 1 // Set to 1 for demo
      val user = User(userId, username)
      val member = Member(memberName, userId) 
      // ---- I NEED TO EXECUTE THIS BLOCK WITHIN TRANSACTION ----
      for {
        userResult <- userRepository.save(user)
        memberRepository.save(member)
      } yield ()     
      // ---- END OF TRANSACTION ----
   }
}

在上面的例子中,userRepository.save(User)memberRepository.save(member)操作应该在事务中执行。

我不想直接在我的服务层使用 slick,因为它会使我的测试复杂化。

此外,我不想在我的单元测试中使用嵌入式数据库,在其他地方这将是一个 NOT 单元测试,我需要完全隔离。

我根本不希望我的存储库接口依赖于 slick,但需要这样的东西:

trait UserRepository {
   findById(id: Long): Future[Option[User]]
   save(user: User): Future[Unit] 
}

如何使用 slick 实现此目的?

好的 - 让我们将您的问题分解为三个部分。

如何在交易中执行区块

基本上看了这个回答:

一旦将 DBIO 转换为 Future,您就完成了。没有机会在单个事务中组合多个操作。故事结束。

如何避免在测试中使用Slick

这基本上是一个设计问题 - 如果您想在 Repository / DAO / 任何东西之上有一个业务层 - 而不是让这个服务层处理事务。您不需要与该层外的 Slick 交互。

避免您的存储库接口依赖于 Slick

以最直接的方式 - 您需要依靠 Slick DBIO 来组合事务中的操作(并且在事务中组合 Repository 方法是在任何严肃的应用程序中都无法避免的事情)。

如果你想避免依赖于 DBIO 你可能会创建你自己的 monadic 类型,比如 TransactionBoundary[T]TransactionContext[T].

然后你会有类似 TransactionManager 的东西来执行这个 TransactionContext[T].

恕我直言不值得付出努力,我只是使用 DBIO 已经有了一个很棒的名字(比如 Haskell 的 IO monad - DBIO 告诉你您有对存储执行的 IO 操作的描述)。但假设您仍然想避免它。

你也许可以做类似的事情:

package transaction {

  object Transactions {
    implicit class TransactionBoundary[T](private[transaction] val dbio: DBIO[T]) {
      // ...
    }
  }

  class TransactionManager {
    def execute[T](boundary: TransactionBoundary[T]): Future[T] = db.run(boundary.dbio)
  }
}

你的特质应该是这样的:

trait UserRepository {
   findById(id: Long): TransactionBoundary[Option[User]]
   save(user: User): TransactionBoundary[Unit] 
}

在你的代码中的某处你会这样做:

transactionManager.execute(
    for {
        userResult <- userRepository.save(user)
        memberRepository.save(member)
    } yield ()  
)

通过使用隐式转换,您可以将 Repository 中方法的结果自动转换为您的 TransactionBoundary

但是再次 - 恕我直言,以上所有内容都没有比使用 DBIO 带来任何实际优势(除了美学品味)。如果你想避免在特定层之外使用 Slick 相关的 类,只需创建一个类型别名,如下所示:

type TransactionBoundary[T] = DBIO[T]

并在任何地方使用它。