Scala Slick Cake Pattern: over 9000 classes?
Asked Answered
W

2

11

I'm developing a Play! 2.2 application in Scala with Slick 2.0 and I'm now tackling the data access aspect, trying to use the Cake Pattern. It seems promising but I really feel like I need to write a huge bunch of classes/traits/objects just to achieve something really simple. So I could use some light on this.

Taking a very simple example with a User concept, the way I understand it is we should have:

case class User(...) //model

class Users extends Table[User]... //Slick Table

object users extends TableQuery[Users] { //Slick Query
//custom queries
}

So far it's totally reasonable. Now we add a "Cake Patternable" UserRepository:

trait UserRepository {
 val userRepo: UserRepository
 class UserRepositoryImpl {
    //Here I can do some stuff with slick
    def findByName(name: String) = {
       users.withFilter(_.name === name).list
    }
  }
}

Then we have a UserService:

trait UserService {
 this: UserRepository =>
val userService: UserService
 class UserServiceImpl { //
    def findByName(name: String) = {
       userRepo.findByName(name)
    }
  }
}

Now we mix all of this in an object :

object UserModule extends UserService with UserRepository {
    val userRepo = new UserRepositoryImpl
    val userService = new UserServiceImpl 
}
  1. Is UserRepository really useful? I could write findByName as a custom query in Users slick object.

  2. Let's say I have another set of classes like this for Customer, and I need to use some UserService features in it.

Should I do:

CustomerService {
this: UserService =>
...
}

or

CustomerService {
val userService = UserModule.userService
...
}
Wanids answered 4/4, 2014 at 15:2 Comment(2)
Where did get 9000 from?Baccate
@Baccate knowyourmeme.com/memes/its-over-9000Wanids
T
10

OK, those sound like good goals:

  • Abstract over the database library (slick, ...)
  • Make the traits unit testable

You could do something like this:

trait UserRepository {
    type User
    def findByName(name: String): User
}

// Implementation using Slick
trait SlickUserRepository extends UserRepository {
    case class User()
    def findByName(name: String) = {
        // Slick code
    }
}

// Implementation using Rough
trait RoughUserRepository extends UserRepository {
    case class User()
    def findByName(name: String) = {
        // Rough code
    }
}

Then for CustomerRepository you could do:

trait CustomerRepository { this: UserRepository =>
}

trait SlickCustomerRepository extends CustomerRepository {
}

trait RoughCustomerRepository extends CustomerRepository {
}

And combine them based on your backend whims:

object UserModuleWithSlick
    extends SlickUserRepository
    with SlickCustomerRepository

object UserModuleWithRough
    extends RoughUserRepository
    with RoughCustomerRepository

You can make unit-testable objects like so:

object CustomerRepositoryTest extends CustomerRepository with UserRepository {
    type User = // some mock type
    def findByName(name: String) = {
        // some mock code
    }
}

You are correct to observe that there is a strong similarity between

trait CustomerRepository { this: UserRepository =>
}

object Module extends UserRepository with CustomerRepository

and

trait CustomerRepository {
    val userRepository: UserRepository
    import userRepository._
}

object UserModule extends UserRepository
object CustomerModule extends CustomerRepository {
    val userRepository: UserModule.type = UserModule
}

This is the old inheritance/aggregation tradeoff, updated for the Scala world. Each approach has advantages and disadvantages. With mixing traits, you will create fewer concrete objects, which can be easier to keep track of (as in above, you only have a single Module object, rather than separate objects for users and customers). On the other hand, traits must be mixed at object creation time, so you couldn't for example take an existing UserRepository and make a CustomerRepository by mixing it in -- if you need to do that, you must use aggregation. Note also that aggregation often requires you to specify singleton-types like above (: UserModule.type) in order for Scala to accept that the path-dependent types are the same. Another power that mixing traits has is that it can handle recursive dependencies -- both the UserModule and the CustomerModule can provide something to and require something from each other. This is also possible with aggregation using lazy vals, but it is more syntactically convenient with mixing traits.

Tussis answered 4/4, 2014 at 20:33 Comment(2)
Thanks for your answer that might be already better. One abstraction I would like is the ability to switch from slick to something else if needed, with as little changes as possible. Also I'd like to be able to test easily all of this that's also why I thought about this pattern which helps mocking classes.Wanids
Thank you for the great explanation and examples it's a lot clearer now. The singleton-types and recursive dependencies were also issues I encountered so it helped for this too. What bothers me with the mixin trait approach is that the modules quickly contain a whole bunch of repositories / services / whatever, because of all the mixed-in dependencies, so when you use a module you end up having access to all those "collateral" classes although you don't necessarily need/want to.Wanids
O
4

Check out my recently published Slick architecture cheat sheet. It does not abstract over the database driver, but it is trivial do change it that way. Just wrap it in

class Profile(profile: JdbcProfile){
  import profile.simple._
  lazy val db = ...
  // <- cheat sheet code here
}

You do not need the cake pattern. Just put it all in one file and you get away without it. The cake pattern allows you to split the code into different files, if you are willing to pay the syntax overhead. People also use the cake pattern to create different configurations including different combinations of services, but I don't think this is relevant to you.

If the repeated syntax overhead per database table bothers you, generate the code. The Slick code-generator is customizable exactly for that purpose:

If you want to blend hand-written and generated code, either feed the hand-written code into the code-generator or use a scheme, where the generated code inherits from hand-written cor vise-versa.

For replacing Slick by something else, replace the DAO methods with queries using another library.

Onida answered 6/4, 2014 at 10:47 Comment(3)
Good template, you are probably right that for this particular case the cake pattern is probably an overkill, but this is a basic example I took for simplicity, the application is actually bigger and could use some modularity. I tried the code generator, it's nice but there are some side-effects issues with the play evolutions for example, I know there are solutions but I don't want to get into that yet. Also I like to completely separate the Business model class (case class User) from any DAO dependency, and having them in separate files helps. But I will use your template for the slick part.Wanids
Yes, we need to provide and out of the box integration of the slick code gen and play (or slick) evolutions. There has been work in this direction here: blog.papauschek.com/2013/12/…Onida
Yes I've seen this it looks promising. I'll probably end up using when it's stable and the app gets too big, but for now the number of model classes I have is not so large and can be managed manually, plus it helps me to really understand what I'm doing :)Wanids

© 2022 - 2024 — McMap. All rights reserved.