scaladesign-patternsplayframeworkslickdeadbolt-2

Play-Slick: Is it possible to improve this design (pattern) ... and how to call it?


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:

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:

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?


Solution

  • 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)?