vendredi 16 décembre 2016

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:

  • 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. the UserRow model type, the UserDao and some business context e.g. the Session.
  • Some UserService methods naturally apply to the 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 getRoles(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.getRoles(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?

Aucun commentaire:

Enregistrer un commentaire