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:
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 5:
I open the browser and use the url http://localhost:9000 to see the application. It works. Great.
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.
The above script contains two parts:
I have implemented SQL queries in Employee and Address companion objects. I have also defined the below parsers to transform a JDBC
In Employee helper object:
In Address helper object:
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:
Here is another example:
In the above code I use DB.withTransaction to manage a transaction for the block.
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
Step 10:
Step 11:
Now lets concentrate on creating views. At present this application has four web pages:
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.- 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)
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 applicationE:\>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 mainapplication.conf
file, theroutes
definition files and themessages
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 applicationE:\>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.
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
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
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.
ReplyDeleteThank you for pointing out this. I have corrected.
ReplyDeleteThank you. This article is very good.
ReplyDeleteThanks for such an informative article and the extensive explanation, it's been very useful.
ReplyDeleteI am happy to know that you have found it useful.
ReplyDeleteThis is very good article, helped a lot..........
ReplyDelete