Slick 3: How to implement repository pattern with transactions?

雨燕双飞 提交于 2019-12-23 08:51:23

问题


In my play framework (2.5) app, I need to write unit tests for services.

I need to isolate data access logic to able to test service layer in isolation, for this I want to create repository interfaces and MOCK them in my unit tests:

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 ----
   }
}

In the above example, userRepository.save(User) and memberRepository.save(member) operations should be performed within transaction.

I don't want to use slick directly in my service layer because it will complicate my tests.

Also, I don't want to use embedded database for my unit tests, elsewhere it would be a NOT unit test, I need complete isolation.

I do not want my repository interfaces to be depended on slick at all, but need something like this:

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

how can I achieve this with slick?


回答1:


OK - let's decompose your question into three parts.

How to execute block in transaction

Basically read this answer: How to use transaction in slick

As soon as you convert DBIO to Future you are done. No chances to compose multiple operation within single transaction. End of story.

How to avoid using Slick in tests

This is basically a design question - if you want to have a business layer on top of Repository / DAO / whatever - than let this service layer deal with transactions. You won't need to interact with Slick outside of this layer.

Avoiding your repository interfaces to depend on Slick

In most straightforward way - you need to depend on Slick DBIO to compose operations within transaction (and composing Repository methods within transaction is something that you cannot avoid in any serious application).

If you want to avoid depending on DBIO you would perhaps create you own monadic type, say TransactionBoundary[T] or TransactionContext[T].

Then you would have something like TransactionManager that would execute this TransactionContext[T].

IMHO not worth the effort, I'd simply use DBIO which has a brilliant name already (like Haskell's IO monad - DBIO informs you that you have a description of IO operations performed on your storage). But let's assume that you still want to avoid it.

You could do something like that perhaps:

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)
  }
}

Your trait would look like this:

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

and somewhere in your code you would do like this:

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

By using implicit conversion you would have your results of methods in Repository be automatically converted to your TransactionBoundary.

But again - IMHO all of the above doesn't bring any actual advantage over using DBIO (except perhaps taste of esthetics). If you want to avoid using Slick related classes outside of certain layer, just make a type alias like this:

type TransactionBoundary[T] = DBIO[T]

and use it everywhere.



来源:https://stackoverflow.com/questions/41618176/slick-3-how-to-implement-repository-pattern-with-transactions

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!