I'm using Play-Slick versions 2.5.x and 3.1.x respectively. I use Slick's code generator and produce the Slick model from an existing database. Actually I'm shy to admit that I'm DB-design driven and not class-design driven.
This is the initial setup:
- Generated Slick model under
generated.Tables._
- Generic Slick dao implementation
- Service layer that builds on top of the Generic Slick dao
These are the forces behind the pattern which I temporary called "Pluggable Service" because it allows plugging in the service layer functionality to the model:
- Play's controllers and views must only see the Service layer (and not the Dao's) e.g.
UserService
- Generated model e.g.
UserRow
is expected to comply to business layer interfaces e.g. Deadbolt-2's Subject but not implement it directly. To be able to implement it one needs "too much" e.g. theUserRow
model type, theUserDao
and potentially some business context. - Some of the
UserService
methods naturally apply to the modelUserRow
instance e.g.loggedUser.roles
orloggedUser.changePassword
Therefore I have:
generated.Tables.scala
Slick model classes:
case class UserRow(id: Long, username: String, firstName: String,
lastName : String, ...) extends EntityAutoInc[Long, UserRow]
dao.UserDao.scala
Dao extensions and customizations specific to the User model:
@Singleton
class UserDao @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)
extends GenericDaoAutoIncImpl[User, UserRow, Long] (dbConfigProvider, User) {
//------------------------------------------------------------------------
def roles(user: UserRow) : Future[Seq[Role]] = {
val action = (for {
role <- SecurityRole
userRole <- UserSecurityRole if role.id === userRole.securityRoleId
user <- User if userRole.userId === user.id
} yield role).result
db.run(action)
}
}
services.UserService.scala
service that facades all user operations to the rest of the Play application:
@Singleton
class UserService @Inject()(auth : PlayAuthenticate, userDao: UserDao) {
// implicitly executes a DBIO and waits indefinitely for
// the Future to complete
import utils.DbExecutionUtils._
//------------------------------------------------------------------------
// Deadbolt-2 Subject implementation expects a List[Role] type
def roles(user: UserRow) : List[Role] = {
val roles = userDao.roles(user)
roles.toList
}
}
services.PluggableUserService.scala
finally the actual "Pluggable" pattern that dynamically attaches service implementations to the model type:
trait PluggableUserService extends be.objectify.deadbolt.scala.models.Subject {
override def roles: List[Role]
}
object PluggableUserService {
implicit class toPluggable(user: UserRow)(implicit userService: UserService)
extends PluggableUserService {
//------------------------------------------------------------------------
override def roles: List[Role] = {
userService.roles(user)
}
}
Finally one can do in the controllers:
@Singleton
class Application @Inject() (implicit
val messagesApi: MessagesApi,
session: Session,
deadbolt: DeadboltActions,
userService: UserService) extends Controller with I18nSupport {
import services.PluggableUserService._
def index = deadbolt.WithAuthRequest()() { implicit request =>
Future {
val user: UserRow = userService.findUserInSession(session)
// auto-magically plugs the service to the model
val roles = user.roles
// ...
Ok(views.html.index)
}
}
Is there any Scala way that could help not having to write the boilerplate code in the Pluggable Service object? does the Pluggable Service name makes sense?
One of the common variant may be a parent trait for your controllers that has something along these lines:
The elephant in the room here is how you get
userService
instance. Well you would need to explicitly require it in your controller constructor (in the same way you do withDeadboltActions
). Alternatively you may bundleDeadboltActions
,UserService
and what else into one class (e.g.ControllerContext
?) and inject this single instance as one constructor parameter (but that's probably another discussion...).After that your controller code would be like this:
both
user
andrequest
is implicit which helps to pass into into inner parts of your application (which is often the case - you bringuser
object to perform some business logic).It doesn't get rid of your
PluggableUserService
per se (logic is still there) but it may help you to easier reuse same logic everywhere in your controllers (as in my experience, you need to have bothUser
together withRoles
more often than not in any real application).EDIT: I got a feeling I didn't quite get your question. You want to avoid boilerplate in
PluggableUserService
or you want to avoid scattering this conversion with use ofPluggableUserService
everywhere, in every controller (IMHO 2nd option is something to be avoided)?