Migration to Play 2

Sun, 02/12/2012 - 11:35 -- manuel

This week, the Delving developers got together in Haarlem with a focus on migrating from the Play 1 framework to its next version, Play 2. This is a technical experience report regarding this migration, and is aimed primarily for developers acquainted with the framework.

Our web platform, the Delving Culture-Hub, is based on Play 1.2.4 and Play Scala 0.9.latest together with the Groovy view templates, and has only very few module dependencies (Press, Gravatar, and two custom modules). The data is kept in MongoDB (we use Casbah + Salat for that purpose), JSON de/serialization is handled by Jerkson, and we do quite some intensive indexing and search via Solr (no special library to talk to it, we have our own binding service).

It has about 180 actions (when counting in the custom modules) and probably more of them "hidden" in API actions (that we are thinking of translating to compiled actions in Play 2).
The source is available at https://github.com/delving/culture-hub.
Roughly in total we've spent three man-weeks on the migration so far, and I think it'll take one week longer to finish it. It took one week to take out the Groovy templates engine from Play 1 & developing an integration module for Play 2, another week to migrate the application "foundation" parts (build, framework extensions, core set-up for models and authentication), and now yet another week in total to migrate the application itself.
The ported Groovy templates engine is almost feature complete now, and we will release a first version soon, when we have been able to test it some more.
Here come my personal impression and comments about Play 2 and the migration process:
1) Development is fast again, thankfully. As the Play 1 version of the Culture-Hub grew, the reload time got longer and longer, until around 2 minutes in the end. This really crippled the development flow - it's easier to loose focus and there are only so many 2 minute videos on Boingboing per day to watch. The reason it was so slow in Play 1 are the bytecode enhancements taking place on Scala classes. For that reason we also had to switch from the Scala templates to the Groovy ones (changing a space in a template meant waiting 2 minutes, which was slowly driving Eric and myself insane).
2) There's a fundamental shift from "very flexible framework" to "type safe framework" that requires quite a change in mentality. Whilst Play 1.x is pretty dynamic and flexible in many ways (mostly due to mutability and the usage of Groovy), Play 2 checks after you like a German Shepherd to make sure everything compiles nicely and will work predictably at runtime.
3) From the Scala perspective, the shift to a functional programming style is a nice thing but requires some changes in the code, e.g.:
- having methods return an Action makes it harder to escape the control flow via return statements (you can return Action { implicit request => Error("Houston") } but it feels a little clumsy), so this is one thing to heck for in longer actions (ideally, just produce a result using if/else/match/getOrElse etc.

- compiling the routes and explicitely having to specify query strings takes some time to get used to

- the two things above are completely forgiven for when starting to use the tremendously cool functional testing API. I strongly recommend writing specs whilst doing the migration, it's just so much fun to see everything go green

- the shift from @Before and @After calls to composed actions takes some energy as well when you make extensive use of them. We had mostly access control taken care of by mixing in traits with @Before's

4) request body parsers and Content-s are an interesting and powerful thing. I really like the level of control they give over the requests, the only small comment I'd have is regarding the API to access data from the request. Many of the actions we have fetch some POSTed url-encoded data (mostly just an ID for example) and then having to write code to check if the body is correctly encoded, and if yes fetch the first value is a bit cumbersome. I started writing implicit helpers like this one to make things easier:
def getFirstAsString(key: String): Option[String] = body.asFormUrlEncoded match { case Some(b) => b.get(key).getOrElse(Seq()).headOption case None => None }
5) Some missings (and ported) libs here, and the emailing here and here
6) Not a problem for us, but it will be for many I believe: modules. I suppose many applications with a lot of module dependencies will be harder to migrate.
7) The form helper is an interesting thing. Some comments:

- I suppose the documentation is updated now, without that, it's a bit hard to understand how it works (unless reading the source of course). So my advice here is to really read the documentation, these forms are quite powerful.

- We used the Play 1 constraint annotations (@Required etc.) to generate the client-side validation. We still have to port this to using the Form helper object, it's nicely powerful that is, once you get it.

8) Iteratees, Enumerators and Enumeratees are powerful beasts. Non-blocking IO and stream transformation will really help us a lot (we upload & parse big XML documents as well as generate & stream them, and so far that has been pretty slow, so this is perfect for us). Some more scala-doc for the methods would be nice to have to get a quicker grasp of them.
Some migration tips:
  • get slowly ready for it and play around with Play 2 before going for a full migration.
  • read the documentation, now that it is there. It is really good!
  • do some cleanup in your Play 1 application first, in order to simplify migration. Remove modules you don't need and / or find ways to replace them before you migrate
  • have a plan: list what "foundation" your application needs to have in order to run (without even having models, controllers etc. - just library dependencies, core extensions, configuration, etc.), and start by building this one. Copy-paste the routes file but comment everything out. Migrate the utilities and model layer first. Make a list of the functionality and see what dependencies exist, then migrate the associated controllers and views bit by bit.
  • always aim at having compiling code. Our process is roughly: pick a controller to port from the routes file, create it anew, copy-pate & comment out the old controller code, then port one action after another. Finally comment in the adapted route. Write specs whenever it makes sense along the way, if you have the time.
  • be pragmatic. It doesn't have to be beautiful at first, make that happen in later iterations.
  • don't get side-tracked and re-invent your application or add new functionality. This is a technical migration only, take advantage of it when your application works again. That being said, this migration is a good opportunity to reflect on your application & remove unnecessary functionality if you can.
Happy migration!