Playing with Play
Published on Aug 06, 2013 by dix.
Two Fridays ago a challenge was posed to me and a deadline was imposed. There is a serious bathroom availability problem in my office. It is often inaccessible for cleaning at crucial times and my coworkers are forced to walk all the way across the office before finding out it is closed. I took the challenge and promised to have an app to track whether the bathroom was open by lunch time.
Bets were made and doubts were stated. Doubts primarily arising from my decision to use the Play framework with Scala. I had no experience with the framework and very little experience with the language. The doubters were correct to doubt me. I did not finish the app before lunchtime, but I did complete it and deploy it shortly after lunch. You can find it here. This experience gave me some perspective on the Play framework which I would like to share.
I use Rails all day every day, and so I encounter Play primarily as a Rails developer. The first few hours using Play with Scala felt a lot like the first few hours I spent in 2008 with Ruby on Rails. It was very friendly to the new developer. So friendly in fact that the entirety of the documentation is bundled with the framework. On the development server, you can view documentation that will take you all the way to deploying the app to Heroku. This is fabulous. For a developer working in a framework for the first time, this guidance is incredible.
Playing with the Database
Working with databases in Scala Play is very different from working in ActiveRecord in Rails. Play provides Anorm for interacting with databases. Anorm is “Anorm is not a Object Relation Manager” and they aren’t Playing. Below is the code I wrote to interact with the Bathroom object.
case class Bathroom(id: Long, label: String, status: Boolean) { def isClosed(): Boolean = this.status } object Bathroom { val bathroom = { get[Long]("id") ~ get[String]("label") ~ get[Boolean]("status") map { case id~label~status => Bathroom(id, label, status) } } def all(): List[Bathroom] = DB.withConnection { implicit c => SQL("SELECT * FROM bathrooms").as(bathroom *) } def create(label: String) { DB.withConnection { implicit c => SQL("INSERT INTO bathrooms (label, status) VALUES ({label}, true)").on( 'label -> label ).executeUpdate() } } def find(id: Long): Bathroom = { DB.withConnection { implicit c => SQL("SELECT * FROM bathrooms WHERE id={id}").on( 'id -> id ).as(bathroom *).head } } def save(bathroom:Bathroom) = DB.withConnection { implicit c => SQL("UPDATE bathrooms set status = {status} where id = {id}").on( 'status -> bathroom.status, 'id -> bathroom.id ).executeUpdate() } }
There is a lot going on here, and it is very different than how things would look in Ruby and Rails. For example, we have to define a parser which transforms database results in classes.
val bathroom = { get[Long]("id") ~ get[String]("label") ~ get[Boolean]("status") map { case id~label~status => Bathroom(id, label, status) } }
This is very tough for me to understand as a new person looking at this framework. I believe that I could use this if I were to write a serious app, but I have no idea how I would even begin to work on Anorm itself.
Playing With Controllers
Working in controllers in Play felt very familiar to me as a Rails developer. Look at this controller and tell me it couldn’t almost be a Rails controller.
object BathroomController extends Controller { val bathroomForm = Form( "label" -> nonEmptyText ) def index = Cached("homePage") { Action { Ok(views.html.index(Bathroom.all(), bathroomForm)) } } def show(id: Long) = Action { implicit request => val bathroom = Bathroom.find(id) Ok(views.html.bathroomShow(bathroom, BathroomStatistic.closedDurations(bathroom), BathroomStatistic.averageClosedDuration(bathroom))) } def create = Action { implicit request => bathroomForm.bindFromRequest.fold( errors => BadRequest(views.html.index(Bathroom.all(), errors)), label => { Bathroom.create(label) Cache.set("homePage", None) Redirect(routes.BathroomController.index) } ) } def toggle(id: Long) = Action { implicit request => val bathroom = Bathroom.find(id) val event = BathroomEvent.toggleForBathroom(bathroom) event.save Bathroom.save(event.apply(bathroom)) Cache.set("homePage", None) Redirect(routes.BathroomController.index) } }
There are a few interesting things here.
val bathroomForm = Form( "label" -> nonEmptyText )
We declare and create a form here which will be used in the view template and to validate the submission. In the create action, we make use of the form in a way that did not make much sense to me at first.
def create = Action { implicit request => bathroomForm.bindFromRequest.fold( errors => BadRequest(views.html.index(Bathroom.all(), errors)), label => { Bathroom.create(label) Cache.set("homePage", None) Redirect(routes.BathroomController.index) } ) }
First, bindFromRequest
is unclear because it relies on an implict variable. It
is helpful to think it as bindFromRequest(request)
. My understanding is that
bindFromRequest
will pull the necessary parameters out of the request,
validate the params, etc. The fold method which is invoked next takes two
arguments. The first is a function which is performed if there are errors on the
form, the second is called when no errors are present. I’m not entirely sure why
the name fold was chosen here, but there you go. You can find the documentation
for the Form object here.
Impressions of Play
Overall, I found working in Play very interesting. I was able to easily accomplish the task I set for myself, and I enjoyed working in the framework. My first encounter with it reminded me a lot of working in Rails for the first time. With the ease of use and the improved performance over Rails, I think it would be a good choice as a general web application framework.