Sunday, 18 March 2012

Playing with Play Framework - PART 1

Last few days I have been playing with the Play Framework. I must say that I am loving it. Recently Play 2.0 is released.  It is a good web framework in many ways: 
  • fast development & feedback loop
  • it is based on event-driven and non-blocking IO
  • it is built for asynchronous programming
  • support for both Java and Scala
  • and more .... (please check http://www.playframework.org
I have also been learning Scala. So I have thought that it would be a good idea to create a sample application using Play 2.0 and Scala.  My intention is to develop this sample application gradually and at the same time demonstrate various aspects of Play Framework (trying Scala in this way is a bonus for me). One thing I should say here I have followed the sample applications that are given with the Play to create this application.

Lets begin the work. But before that a short introduction of the sample application.

Introduction

My application is called HR Solutions. This application will keep track of details of employees of an organisation. HR Solutions will be used by two group of users: human resource managers and employees. Human resource managers are responsible for capturing the details of employees. They can create, update, read and delete an employee. Employees will get view permission to see their own details and they can only update their personal details when logged in (this will be added in future). As mentioned earlier I will gradually add other details in later post. Here is the initial domain diagram:



Steps

Here are the steps I have followed to create this application:

Step 1: 

I have downloaded the Play 2.0 from this link download.playframework.org/releases/play-2.0.zip  I have java version 1.7.0_01 in my machine. You need minimum java 1.6 or later.

Step 2: 

Unzip it and add the play directory to the working path. To check that the installation worked, I open a new command line and type play; it shows me the play basic usage help. So everything is ok to create my application.

Step 3: 
I execute the below command to create the application

E:\>play new hr-system


Now I use intellj to open this application to update. The play new command creates a new directory hr-system/ . Play also creates all the necessary files and directories:
  • app/  contains the application’s core, split between models, controllers and views directories. This is the directory where .scala source files live.
  • conf/ contains all the application’s configuration files, especially the main application.conf file, the routes definition files and the messages files used for internationalization.
  • project contains the build scripts. The build system is based on sbt (Simple Build Tool).
  • public/ contains all the publicly available resources, which includes JavaScript, stylesheets and images directories.

Step 4: 
Below commands are used to run the newly created application

E:\>cd hr-system

E:\hr-system>play

[hr-system] $ run


Step 5:

I open the browser and use the url http://localhost:9000 to see the application. It works. Great. 

Step 6:
To capture the details of the employees I need a database. I am going to use Play provided H2 for this. It is useful since I am going to do lot of testing and changes to my domain model. To set up the database I open hr-system/conf/application.conf and uncomment the below two lines:

db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play" 

Step 7:

It is useful to keep track and organize the database schema evolution. Play provides a way to do this using several evolution scripts. In my case I am going to add one script now, 1.sql under hr-system/conf/evolutions/default

# --- !Ups

set ignorecase true;

CREATE TABLE employee (
  id                             bigint not null auto_increment,
  name                           varchar(255) not null,
  age                            integer,
  gender                         char(1),
  address_id                     bigint,
  constraint pk_employee primary key (id)
);

CREATE TABLE address (
  id                             bigint not null auto_increment,
  street                         varchar(255) not null,
  city                           varchar(255),
  postcode                       varchar(30) not null,
  constraint pk_address primary key (id)
);

alter table employee add constraint fk_employee_address_1 foreign
key (address_id) references address (id) on delete restrict;

# --- !Downs

SET REFERENTIAL_INTEGRITY FALSE;

drop table if exists address;

drop table if exists employee;

SET REFERENTIAL_INTEGRITY TRUE;

The above script contains two parts:
  • The Ups part the describe the required transformations.
  • The Downs part that describe how to revert them.
Step 8:
Now it is time to create the model class and also write necessary code to query the database. I will use Anorm to interact with the database. I create Employee and Address case classes and also their helper objects in a file Employee.scala under hr-system/app/models/ .  First let me show the case classes:
 

case class Employee (
   id: Pk[Long] = NotAssigned,
   name: String,
   age:Number,
   gender:String,
   addressId:Option[Long]
)

case class Address (
   id: Pk[Long] = NotAssigned,
   street: String,
   city:String,
   postcode:String
)

I have implemented SQL queries in Employee and Address companion objects. I have also defined the below parsers to transform a JDBC ResultSet row to a Employee and Address value. 

In Employee helper object:


  /**
   * Parse a Employee from a ResultSet
   */
  val simple = {
     get[Pk[Long]]("employee.id") ~
     get[String]("employee.name") ~
     get[Int]("employee.age") ~
     get[String]("employee.gender") ~
     get[Option[Long]]("employee.address_id") map {
        case id~name~age~gender~addressId => 
          Employee(id, name, age, gender, addressId)
     }
  }

  /**
   * Parse a (Employee,Address) from a ResultSet
   */
  val withAddress = Employee.simple ~ Address.simple  map {
     case employee~address => (employee,address)
  }

In Address helper object:


 /**
   * Parse a Address from a ResultSet
   */
  val simple = {
      get[Pk[Long]]("address.id") ~
      get[String]("address.street") ~
      get[String]("address.city") ~
      get[String]("address.postcode") map {
        case id~street~city~postcode => Address(id, street, city, postcode)
      }
  }

Now let me show how I have used these parsers. For example the below method is responsible to return a list of employees with their address:


 /**
   *  Find all the employees
   */
  def all(): List[(Employee, Address)]= {
     DB.withConnection { implicit connection =>
     SQL("select * from employee join address on " +
     "employee.address_id = address.id").as(Employee.withAddress *)
  }

In Play we have DB.withConnection helper to create and release a JDBC connection automatically. In the above method I use the Anorm SQL method to create the query. The as method is used to parse the ResultSet using the Employee.withAddress * parser. It then return a List[(Employee, Address)]. The connection will be automatically closed at the end of the block.

Here is another example:


 /**
   *  Create an employee
   *
   *  @param name - Name of the employee
   *  @param age - Age of the employee
   *  @param gender - Gender of the employee
   *  @param street - Street of the employee
   *  @param city - City of the employee
   *  @param postcode - Postcode of the employee
   *
   */
  def create(name:String,  age:Number, gender:String,  
             street:String, city:String, postcode:String): Unit = {

     DB.withTransaction { implicit connection =>

        SQL("insert into address(street,city,postcode) " +
                "values ({street}, {city}, {postcode})").on(
           'street -> street,
           'city -> city,
           'postcode -> postcode
        ).executeUpdate()

        val addressId:Long = SQL("select id from address " +
                "order by id desc limit 1").as(scalar[Long].single)

        SQL("insert into employee(name,age,gender, address_id) " +
                "values ({name}, {age}, {gender}, {address_id})").on(
           'name -> name,
           'age -> age,
           'gender -> gender,
           'address_id -> addressId
        ).executeUpdate()
    }
  }

In the above code I use DB.withTransaction to manage a transaction for the block.

Step 9:
After creating the model class I create EmployeeController.scala under hr-system/app/controllers 


/**
 * This controller is responsible to manage employees' records
 */
object EmployeeController extends Controller {

  /**
   *  Employee form for creating and editing an employee
   */
  val employeeForm = Form(
     tuple(
        "name" -> text,
        "age" -> number,
        "gender" -> text,
        "street" -> text,
        "city" -> text,
        "postcode" -> text
     )
  )

  /**
   * Display the list of employee
   */

  def employees = Action {
     Ok(html.index(Employee.all()))
  }

  /**
   * Display the employee form to create an employee
   */
  def createForm = Action {
    Ok(html.createForm(employeeForm))
  }

  /**
   * Handle the employee form submission
   */
  def save = Action { implicit request =>
     employeeForm.bindFromRequest.fold(
        errors => BadRequest(html.createForm(errors)),
        {
           case (name, age, gender, street, city, postcode) =>
              Employee.create(name, age, gender, street, city, postcode)
              Redirect(routes.Application.index())
        }
     )
  }

  /**
   * Display the employee form to edit
   * @param id - Id of the employee to edit
   */
  def editForm(id: Long) = Action {
     val employeeMap = new HashMap[Int, String]
     Employee.findById(id).map {
        case (employee, address) => {
           employeeMap += 1 -> employee.name
           employeeMap += 2 -> employee.age.toString
           employeeMap += 3 -> employee.gender
           employeeMap += 4 -> address.street
           employeeMap += 5 -> address.city
           employeeMap += 6 -> address.postcode
        }
     }
     Ok(html.editForm(id, employeeForm.fill((employeeMap(1), 
  employeeMap(2).toInt, employeeMap(3), employeeMap(4), employeeMap(5),
 employeeMap(6)))))
  } 

  /**
   * Handle the employee form submission to edit
   * @param id - Id of the employee to be updated
   */
  def update(id: Long) = Action { implicit request =>
     employeeForm.bindFromRequest.fold(
        errors => BadRequest(html.editForm(id, errors)),
        {
           case (name, age, gender, street, city, postcode) =>
            Employee.update(id, name, age, gender, street, city, postcode)
              Redirect(routes.Application.index())
        }
    )
  }

  /**
   * Handle the request to delete an employee
   * @param id - Id of the employee to be deleted
   */
  def delete(id: Long) = Action {
     Employee.delete(id)
     Redirect(routes.Application.index())
  }
}

In the above controller class you will find I have used employerForm to capture all the data. The type of the employeeForm is Form[(String, Int, String, String, String, String)]. Though not shown above I have imported play.api.data classes. I use bindFromRequest to fill the form with the request data. In case of errors I redisplay it with BadRequest 400. I also use Redirect instead of Ok to specify a 303 See Other HTTP response type. 


Step 10:

Next step is to define router. The router is in charge of translating each incoming HTTP request to an Action. An HTTP request is seen as an event by the MVC framework. Each event contains two major pieces of information:
  • the request path including the query string. For example: /employees/13,
  • the HTTP method i.e. GET, POST, PUT, HEAD, etc.
All the routes are defined in hr-system/conf/routes file. Each route consists of an HTTP method and URI pattern, both associated with a call to an Action generator.


# Home page
GET /                         controllers.Application.index

# Employees
GET /employees                controllers.EmployeeController.employees()

# Add an employee
GET  /createForm              controllers.EmployeeController.createForm()
POST /save                    controllers.EmployeeController.save()

# Edit an employee
GET /employees/:id/editForm  controllers.EmployeeController.editForm(id:Long)
POST /employees/:id          controllers.EmployeeController.update(id:Long)

# Delete an employee
POST /employees/:id/delete  controllers.EmployeeController.delete(id:Long)


Step 11:

Now lets concentrate on creating views. At present this application has four web pages: 
  • main.scala.html template contains all the necessary includes.
  • index.scala.html template shows the list of employees
  • create.scala.html template is for creating an employee's record
  • edit.scala.html template is for editing
All these templates are added under hr-system/app/views. Here I am showing only create.scala.html and index.scala.html. Others you can see here.

create.scala.html


@(employeeForm: Form[(String, Int, String, String, String, String)])

@import helper._
@import helper.twitterBootstrap._

@main(title = "HR Solutions") {

@form(action = routes.EmployeeController.save) {

<fieldset>
    <:legend>Add Employee</legend>

    @inputText(
        field = employeeForm("name"),
        args = '_label -> "Name"
    )

    @inputText(
        field = employeeForm("age"),
        args = '_label -> "Age", '_size -> 3
    )

    @select(
        field = employeeForm("gender"),
        options = options(
            "" -> "Please select",
            "m" -> "Male",
            "f" -> "Female"
        ),
        args = '_label -> "Gender"
    )

    @inputText(
        field = employeeForm("street"),
        args = '_label -> "Street"
    )

    @inputText(
        field = employeeForm("city"),
        args = '_label -> "City"
    )

    @inputText(
        field = employeeForm("postcode"),
        args = '_label -> "Post Code"
    )

    <br/>

    <div class="actions">
        <input type="submit" value="Submit">
        <a href="@routes.Application.index">Cancel</a>
    </div>
</fieldset>
}
}


index.scala.html


@(employees: List[(Employee, Address)])

@import helper._

@main(title = "HR Solutions"){
<center>
<h2>HR Solutions</h2>

<h3>Total number of Employees: @employees.size</h3>

<table width="60%" border="1" align="center" text-align="center">
    <thead>
    <tr>
        <th>Name</th>
        <th>Age</th>
        <th>Gender</th>
        <th>Street</th>
        <th>City</th>
        <th>Postcode</th>
        <th>Edit</th>
        <th>Delete</th>
    </tr>
    </thead>
    <tbody>
    @employees.map {
        case (employee, address) => {
        <tr align="center">
            <td>@employee.name</td>
            <td>@employee.age</td>
            <td>
                @if(employee.gender.equals("m")) {
                   Male
                } else {
                   Female
                }
            </td>
            <td>@address.street</td>
            <td>@address.city</td>
            <td>@address.postcode</td>
            <td>
      @form(action = routes.EmployeeController.editForm(employee.id.get)) {
         <input type="submit" value="Edit">
      }
            </td>
            <td>
      @form(routes.EmployeeController.delete(employee.id.get)) {
         <input type="submit" value="Delete">
      }
            </td>
        </tr>
        }
    }
    </tbody>
</table>
<br/>
<a href="/createForm">Add a new employee</a>
</center>
}


Conclusion
In this article I have demonstrated how to create a CRUD application using Play 2.0 framwork. It is a good and productive learning phase for me. As I am new to Scala and Play you may find some mistakes in the code. So your feedback is most welcome.  In future articles I will extend and improve this application by adding other features using Play. Please check the code https://github.com/sanjoykroy/hr-system

6 comments:

  1. You mention that you use HSQLDB but in fact the code snippet says you're using H2 (which is a perfectly good idea - I love H2). I imagine this is simply a typo that needs correcting in your post, so I'm pointing it out purely FYI.

    ReplyDelete
  2. Thank you for pointing out this. I have corrected.

    ReplyDelete
  3. Thank you. This article is very good.

    ReplyDelete
  4. Thanks for such an informative article and the extensive explanation, it's been very useful.

    ReplyDelete
  5. I am happy to know that you have found it useful.

    ReplyDelete
  6. This is very good article, helped a lot..........

    ReplyDelete