This time last year, I decided I wanted to learn Scala. I chose the Play Framework as my vehicle for learning and I added CoffeeScript and Jade to the mix. I packaged it all up, learned a bunch and presented it at Devoxx 2011.
In January, I added SecureSocial, JSON Services and worked a bit on the mobile client. I presented my findings at Jfokus shortly after. As part of my aforementioned post, I wrote:
Right before we left for Jfokus, I was able to get everything to work, but didn't spend as much time as I'd like working on the mobile client. If this talk gets accepted for Devoxx France, I plan on spending most of my time enhancing the mobile client.
I had some complications (a.k.a. too much vacation) with Devoxx France and wasn't able to attend. To make up for it, I submitted the talk to ÜberConf. It got accepted and I started working on my app a couple weeks ago. So far, I've spent about 8 hours upgrading it to Play 2 and I hope to start re-writing the mobile client later this week. I plan on using Cordova, jQTouch and releasing it in the App Store sometime this month.
Upgrading to Play 2
When I heard about Play 2, I thought it was a great thing. The developers were re-writing the framework to use Scala at the core and I was already using Scala in my app. Then I learned they were going to throw backwards compatibility out the window and I groaned. "Really? Another web framework (like Tapestry of old) screwing its users and making them learn everything again?!", I thought. "Maybe they should call it Run instead of Play, leaving the old framework that everyone loves intact."
However, after hearing about it at Devoxx and Jfokus, I figured I should at least try to migrate. I downloaded Play 2.0.1, created a new project and went to work.
The first thing I learned about upgrading from Play 1.x to Play 2.x is there's no such thing. It's like saying you upgraded from Struts 1 to Struts 2 or Tapestry 4 to Tapestry 5. It's a migration, with a whole new project.
Evolutions
I started by looking around to see if anyone had documented a similar migration. I found two very useful resources right off the bat:
From Jan's Blog, I learned to copy my evolutions from my Play 1.x project into conf/evolutions/default. I changed my application.conf to use PostgreSQL and wrote an EvolutionsTest.scala to verify creating the tables worked.
import org.specs2.mutable._
import play.api.db.DB
import play.api.Play.current
import anorm._
import play.api.test._
import play.api.test.Helpers._
class EvolutionsTest extends Specification {
"Evolutions" should {
"be applied without errors" in {
evolutionFor("default")
running(FakeApplication()) {
DB.withConnection {
implicit connection =>
SQL("select count(1) from athlete").execute()
SQL("select count(1) from workout").execute()
SQL("select count(1) from comment").execute()
}
}
success
}
}
}
Then I began looking for how to load seed data with Play 2.x. In Play 1.x, you could use a BootStrap job that would load sample data with YAML.
import play.jobs._
import play.Play
@OnApplicationStart
class BootStrap extends Job {
override def doJob() {
import models._
import play.test._
// Import initial data if the database is empty
if (Athlete.count().single() == 0) {
Yaml[List[Any]]("initial-data.yml").foreach {
_ match {
case a: Athlete => Athlete.create(a)
case w: Workout => Workout.create(w)
case c: Comment => Comment.create(c)
}
}
}
}
}
This is no longer a recommended practice in Play 2. Instead, they recommend you turn your YAML into code. 10 minutes later, I had a Global.scala that loaded seed data.
import models._
import play.api._
import play.api.Play.current
import anorm._
object Global extends GlobalSettings {
override def onStart(app: Application) {
InitialData.insert()
}
}
/**
* Initial set of data to be loaded
*/
object InitialData {
def date(str: String) = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(str)
def insert() {
if (Athlete.count() == 0) {
Seq(
Athlete(Id(1), "[email protected]", "beer", "Matt", "Raible"),
Athlete(Id(2), "[email protected]", "whiskey", "Trish", "McGinity")
).foreach(Athlete.create)
Seq(
Workout(Id(1), "Chainsaw Trail",
"""
A beautiful fall ride: cool breezes, awesome views and yellow leaves.
Would do it again in a heartbeat.
""", 7, 90, date("2011-10-13"), 1),
Workout(Id(2), "Monarch Lake Trail",
"Awesome morning ride through falling yellow leaves and cool fall breezes.",
4, 90, date("2011-10-15"), 1),
Workout(Id(3), "Creekside to Flume to Chainsaw",
"Awesome morning ride through falling yellow leaves and cool fall breezes.",
12, 150, date("2011-10-16"), 2)
).foreach(Workout.create)
Seq(
Comment(1, "Jim", "Nice day for it!"),
Comment(2, "Joe", "Love that trail."),
Comment(2, "Jack", "Where there any kittens there?")
).foreach(Comment.create)
}
}
}
Anorm's Missing Magic
Before starting with Play 2, I knew it had lost some of its magic. After all, the developers had mentioned they wanted to get ride of the magic and moving to Scala allowed them to do that. However, I didn't think I'd miss Magic[T] as much as I do. Like Martin Fowler, I like ORMs and having to use SQL again seems painful. It seems like a strange shift for Play to reduce type-safety on the backend, but increase it in its templates. Oh well, to each their own. I may eventually move to Squery, but I wanted to do a side-by-side comparison as part of my migration.
Using the aforementioned tutorial from James and Jan's blog posts, as well as Guillaume's Play 2.0/Anorm, I set about creating new model objects. I wrote a bunch of SQL, typed up some new finders and migrated my tests from ScalaTest to the new default, specs2. The Mosh Pit's Migrating a Play 1.2 website to Play 2.0 was a great help in migrating tests.
That's when I started having issues with Anorm and figuring out how its parser syntax works. After struggling for a few days, I finally found yabe-play20-scala. Since I'd used the yabe tutorial from Play 1.x, it was familiar and helped me get past my problems. Now, things aren't perfect (Workouts aren't ordered by their posted date), but everything compiles and tests pass.
To illustrate how little code was required for Anorm 1.x, checkout Workout.scala in Play 1.x vs. Play 2.x. The Play 1.x version is 66 lines; Play 2.x requires 193 lines. I don't know about you, but I kinda like a little magic in my frameworks to reduce the amount of code I have to maintain.
I was pleasantly surprised by specs2. First of all, it was an easy migration from ScalaTest. Secondly, Play's FakeApplication made it very easy to write unit tests. The line count on my UnitTests.scala in Play 1.x vs. Play 2.x is almost identical.
Summary
The first few hours of developing with Play 2 were frustrating, mostly because I felt like I had to learn everything over again. However, I was pleased to find good references on migrating from Play 1.x. Last night, I migrated all my Controllers, integrated Scalate and got most of my views rendering. I still have issues rendering validation errors, but I hope to figure that out soon. The last 2 hours have been much more fun and I feel like my Scala skills are coming along. I think if the Play Team could eliminate those first few hours of struggling (and provide almost instant joy like Play 1.x) they'd really be onto something.
As soon as I figure out how to validation and how to add a body class based on the URL, I'll write another post on the rest of my migration. A Play 2-compatible version of SecureSocial just came out this evening, so I may integrate that as well. In the meantime, I'll be working on the iPhone app and finishing up a Grails 2 application for James Ward and my Grails vs. Play Smackdown.