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.Tables._
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:
UserService
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. the UserRow
model type, the UserDao
and potentially some business context.UserService
methods naturally apply to the model UserRow
instance e.g. loggedUser.roles
or loggedUser.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:
def MyAction[A](bodyParser: BodyParser[A] = parse.anyContent)
(block: (UserWithRoles) => (AuthenticatedRequest[A]) => Future[Result]): Action[A] = {
deadbolt.WithAuthRequest()(bodyParser) { request =>
val user: UserRow = userService.findUserInSession(session)
// this may be as you had it originally
// but I don't see a reason not to
// simply pull it explicitly from db or
// to have it in the session together with roles in the first place (as below UserWithRoles class)
val roles = user.roles
block(UserWithRoles(user, roles))(request)
}
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 with DeadboltActions
). Alternatively you may bundle DeadboltActions
, 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:
def index = MyAction() { implicit user => implicit request =>
Future {
// ...
Ok(views.html.index)
}
}
both user
and request
is implicit which helps to pass into into inner parts of your application (which is often the case - you bring user
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 both User
together with Roles
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 of PluggableUserService
everywhere, in every controller (IMHO 2nd option is something to be avoided)?