7
votes

I know scala, as a funcional language, is supposed to work differently from a common OO language, such as Java, but I'm sure there has to be a way to wrap a group of database changes in a single transaction, ensuring atomicity as well as every other ACID property.

As explained in the slick docs (http://slick.lightbend.com/doc/3.1.0/dbio.html), DBIOAction allows to group db operations in a transaction like this:

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)

However, my use case (and most real world examples I can think of), I have a code structure with a Controller, which exposes the code for my REST endpoint, that controller calls multiple services and each service will delegate database operations to DAOs.

A rough example of my usual code structure:

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

This is a simple example, but most have more complex business logic in the service layer.

I don't want:

  1. My DAOs to have business logic and decide which database operations to run.
  2. Return DBAction from my DAOs and expose the persistency classes. That completely defeats the purpose of using DAOs in the first place and makes further refactorings much harder.

But I definitely want a transaction around my entire Controller, to ensure that if any code fails, all the changes done in the execution of that method will be rolled back.

How can I implement full controller transactionality with Slick in a Scala Play application? I can't seem to find any documentation on how to do that.

Also, how can I disable auto-commit in slick? I'm sure there is a way and I'm just missing something.

EDIT:

So reading a bit more about it, I feel now I understand better how slick uses connections to the database and sessions. This helped a lot: http://tastefulcode.com/2015/03/19/modern-database-access-scala-slick/.

What I'm doing is a case of composing in futures and, based on this article, there's no way to use the same connection and session for multiple operation of the kind.

Problem is: I really can't use any other kind of composition. I have considerable business logic that needs to be executed in between queries.

I guess I can change my code to allow me to use action composition, but as I mentioned before, that forces me to code my business logic with aspects like transactionality in mind. That shouldn't happen. It pollutes the business code and it makes writing tests a lot harder.

Any workaround this issue? Any git project out there that sorts this out that I missed? Or, more drastic, any other persistence framework that supports this? From what I've read, Anorm supports this nicely, but I may be misunderstanding it and don't want to change framework to find out it doesn't (like it happened with Slick).

1

1 Answers

5
votes

There is no such thing as transactional annotations or the like in slick. Your second "do not want" is actually the way to go. It's totally reasonable to return DBIO[User] from your DAO which does not defeat their purpose at all. It's the way slick works.

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)
  }
}
  • The signature of save in your service class is still the same.
  • No db related stuff in controllers.
  • You have full control of transactions.
  • I cannot find a case where the code above is harder to maintain / refactor compared to your original example.

There is also a quite exhaustive discussion that might be interesting for you. See Slick 3.0 withTransaction blocks are required to interact with libraries.