Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Moderniser une application web avec Scala, Play...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Moderniser une application web avec Scala, Play et Mongo DB

Une présentation que j'ai donnée à Scala.io 2014.

Avatar for Nouhoum Traoré

Nouhoum Traoré

October 23, 2014
Tweet

Other Decks in Programming

Transcript

  1. Moderniser une application web avec Scala, Play et Mongo DB

    Nouhoum Traoré arolla @nouhoumtraore
  2. Plan • Contexte • Architecture • Migration des données •

    Tests • Bonnes pratiques • L’équipe et Scala
  3. Nos contraintes • Itérer rapidement • S’adapter à différents canaux

    de diffusion • Fournir une bonne expérience utilisateur
  4. Pourquoi Play ? • Embrasse HTTP • Templating type safe

    et refactoring • Hot reload • Scala / JVM • Asynchrone (réactif) !
  5. Pourquoi Mongo DB ? • Document / Objet • Flexibilité

    du schéma • Asynchrone (réactif) !
  6. Modèle de données DAO Batchs Actions Struts SQL Server App

    externe Home page Version Mobile Partenaires Espace Candidats Recherche et liste Service
  7. Ce qui ne nous va pas ! • Peu de

    place laissée aux APIs • Adhésion à la base de données • Technologies vieillissantes • Application monolithique
  8. Que faire et comment ? Modèle de données DAO Batchs

    Actions Struts SQL Server App externe Home page Version Mobile Partenaires Espace Candidats Recherche et liste Service
  9. Que faire et comment ? Modèle de données DAO Batchs

    Actions Struts App externe Home page Partenaires Recherche et liste Service Espace Candidats Version Mobile REST API Mongo DB SQL Server
  10. Vue macro Load balancer Struts monolithique REST API Play !

    Espace Candidats Play ! Mongo DB SQL Server
  11. État des lieux • +10 ans de données candidats •

    Crucial pour le business • Interdit de mal faire
  12. Mise en oeuvre • Un acteur par type de donnée

    • Paralléliser quand c’est possible • Arrêt de la base SQL au basculement vers la prod !
  13. Test API : l’approche • Ne pas mocker la base

    • Mongo DB embarqué • Lancé une fois pour tous les tests
  14. Test API : la mise en oeuvre import play.api.test._ object

    MongoSetup { lazy val Config = Map( "mongodb.servers" -> List("localhost:12345"), "mongodb.db" -> "test" ) def appWithEmbeddedMongo = FakeApplication( additionalConfiguration = Config ) }
  15. Test API : la mise en oeuvre import mongo.MongoSetup._ import

    play.api.test.{WithServer, PlaySpecification} case class WithApiServer() extends WithServer( port = 3333, app = appWithEmbeddedMongo )
  16. Test API : lancer Mongo testOptions in IntegrationTest += Tests.Setup(

    () => mongo.MongoLauncher.start ) testOptions in IntegrationTest += Tests.Cleanup( () => mongo.MongoLauncher.stop )
  17. Test API import play.api.test._ trait ApiTest extends PlaySpecification { lazy

    val BaseUrl = "http://localhost:3333" lazy val injector = Guice.createInjector( new TestModule() ) def Api = WithApiServer() }
  18. Test API : l’exemple class SignupApiSpec extends ApiTest { val

    RequestUrl = s"$BaseUrl/accounts" s"POST on $RequestUrl" should { "create an account with the provided email and password" in Api { val data = Json.obj( "email" -> "[email protected]", "password" -> "secret" ) val response = await(WS.url(RequestUrl).post(data)) response.status must be equalTo CREATED (response.json \ "message").as[String] === Messages("info.signup.success") } }
  19. Le frontend • Client de l’API • Zéro accès à

    la DB • Essentiellement du JavaScript
  20. Le frontend : asset pipeline curl http://keljob.com/assets/js/e454f1013e30b783818c8efaf3a8e3a5-startup.js HTTP/1.1 200 OK

    Cache-Control: public, max-age=31536000 Content-Length: 171997 Content-Type: application/javascript; charset=utf-8 Date: Tue, 14 Oct 2014 22:39:23 GMT ETag: e454f1013e30b783818c8efaf3a8e3a5 Last-Modified: Wed, 08 Oct 2014 12:35:10 GMT
  21. API : des liens dans le JSON implicit val AccountWrites

    = new Writes[Account] { override def writes(account: Account): JsValue = Json.obj( "id" -> account.id, "email" -> account.email, "creationDate" -> account.creationDate.toString(), "links" -> Json.obj( "self" -> routes.AccountDetailCtrl.get(account.id).url, "alerts" -> routes.JobAlertDetailCtrl.getAlertsOf(account.id).url, "cv" -> routes.CvDetailController.getCvOf(account.id).url ) )
  22. Les contrôleurs @Singleton class AccountValidationController @Inject() ( accountValidator: AccountValidator) extends

    Controller { def validateAccount(token: String) = Action.async { request => accountValidator.validate(token).map { case Some(account) => Ok(Json.toJson(account)) case _ => UnprocessableEntity(Json.toJson(InvalidToken)) } }
  23. Les services class AccountAuthenticator @Inject() (...) class AccountValidator @Inject() (...)

    class AccountCreator @Inject() (...) class AccountSettingsUpdater @Inject() (...)
  24. Les acteurs import akka.actor._ ... class CvExporter(...) extends Actor {

    def commonBehavior(): Receive = ??? def deleteBehavior(): Receive = ??? def updateBehavior(): Receive = ??? def receive = commonBehavior orElse updateBehavior orElse deleteBehavior }
  25. Gestion des erreurs : services sealed trait NewsletterError case object

    InvalidNewsletterActivationToken extends NewsletterError case object NewsletterUpdateError extends NewsletterError class NewsletterActivator @Inject() (...) { def activate(code: String): Future[Either[NewsletterError, Boolean]] = ??? }
  26. Gestion des erreurs : contrôleurs class NewsletterActivationController ( newsletterActivor: NewsletterActivator)

    extends Controller { def activate(token: String) = Action.async { request => newsletterActivor.activate(token).map { … case InvalidNewsletterActivationToken => ??? case NewsletterUpdateError => ??? … }.recoverApiError("Oops !!!") } }
  27. Gestion des erreurs : contrôleurs implicit class ApiErrorRecover(result: Future[Result]) {

    def recoverApiError(message: String) = result recover { case NonFatal(e) => InternalServerError( Json.toJson(SimpleError(message)) ) } }
  28. L’équipe : recette simple • Vouloir sortir de sa zone

    de confort • Préférer la simplicité… • Communiquer et s’entraider