ScalaTodoList - opensas/Play20Es GitHub Wiki

Su primer aplicación Play

Vamos a escribir una simple aplicación con Play 2.0 y desplegarla en la nube.

Prerequisitos

Antes que nada, asegúrese de tener una instalación de Play funcional. Solo necesita Java (al menos versión 6) y descomprimir los binarios de Play para empezar a trabajar; todo está incluído.

Como vamos a utilizar mucho la línea de comandos, es mejor use un sistema operativo semejante a Unix. Si usa Windows, también funcionará bien; solo que necesitará tipear algunos comandos en la terminal.

Necesitará al menos un editor de texto. Si desea usar un IDE para Java, como Eclipse o IntelliJ, por supuesto que podrá usarlo. Sin embargo, con Play va a divertirse más trabajando con un editor de texto simple como TextMate, Emacs o vi, gracias a que el framework se encarga de la compilación y el despliegue de la aplicación por sí mismo.

Creación del proyecto

Ahora que Play está instalado correctamente, es hora de crear una aplicación. Crear una aplicación Play es bastante fácil y todo el proceso puede llevarse a cabo usando las utilidades de línea de comandos de Play. Esto fomenta un esquema de proyecto estándar común a todas las aplicaciones Play.

Abra una terminal nueva y escriba:

$ play new todolist

La utilidad de Play le hará algunas preguntas. Escoja el template Create a simple Scala application_.

El comando play new crea una nueva carpeta todolist/ con una serie de archivos y carpetas. Los más importantes son los siguientes:

  • app/ contiene el núcleo de la aplicación, repartido entre las carpetas para los modelos, los controladores y las vistas. Este directorio es donde residen los archivos de código fuente de Scala.
  • conf/ contiene todos los archivos de configuración de la aplicación, especialmente el archivo principal application.conf, los archivos routes que definen las rutas y el archivo messages usado para la internacionalización de los mensajes.
  • project contiene los scripts de build. El sistema de build está basado en sbt. Cada nueva aplicación de Play viene provista con un script de build por defecto que funcionará sin necesidad de modificaciones en la mayor parte de los casos.
  • project contains the build scripts. The build system is based on sbt. But a new play application comes with a default build script that will just works fine for our application.
  • public/ contiene todos los recursos públicos, lo cual incluye archivos JavaScript, stylesheets y carpeta de imágenes.
  • test/ contains all the application tests. Tests are written either as Specs2 specifications.
  • test/ contiene todos los test de la aplicación. Los test pueden estar escritos como especificaciones de Specs2.

Dado que Play usa UTF-8 como única codificación, es importante que todos los archivos guardados en esas carpetas usen esa codificación. Asegúrese de configurar adecuadamente su editor de texto.

Usando la consola de Play

Una vez que tenemos la aplicación creada, podemos ejecutar la consola de Play. Vaya a la nueva carpeta todolist/ y ejecute:

$ play

Esto inicia la consola de Play. Son varias las cosas que podemos hacer desde esta consola, pero empecemos por iniciar nuestra aplicación. Desde la consola, escriba run:

[todolist] $ run

Ahora la aplicación está corriendo en modo desarrollo. Abra el browser en http://localhost:9000/::

Nota: Lea más acerca de la consola de Play.

Visión General

Vamos a ver cómo hace la aplicación para mostrarnos esta página.

El punto de entrada principal de la aplicación es el archivo conf/routes. Este archivo define todas las URL accesibles desde su aplicación. Si abre el archivo verá esta primera ruta:

GET	/       controllers.Application.index

Esto simplemente le dice a Play que cuando el servidor reciba una solicitud GET para la dirección /, deberá obtener la Action a ejecutar llamando al método controllers.Application.index.

Veamos cómo son los métodos de controllers.Application.index. Abra el archivo todolist/app/controllers/Application.Scala:

package controllers

import play.api._
import play.api.mvc._

object Application extends Controller {
  
  def index = Action {
    Ok(views.html.index("Your new application is ready."))
  }
  
}

Puede ver que controllers.Application.index() devuelve un Action que se encargará de atender el pedido HTTP. Todos los métodos de tipo Action deben devolver un Result, que representa la respuesta a enviar al browser.

Nota: Lea más acerca de las Actions.

Aquí, el action devuelve un código HTTP 200 OK con una respuesta en HTML, provista por un template. Los templates de Play son compilados a funciones estándar de Scala, en este caso views.html.index(message: String).

Este template está definido en el archivo app/views/index.scala.html:

@(message: String)

@main("Welcome to Play 2.0") {
    
    @play20.welcome(message)
    
}

La primera línea define la firma de la función, esto es la cantidad y tipos de parámetros que recibe. En este caso toma solamente un parámetro de tipo String. Luego el template combina contenido HTML (u otro contenido de texto) con código Scala. Las sentencias en Scala empiezan con el caracter @.

Flujo de Trabajo durante el Desarrollo

Ahora vamos a hacer algunas modificaciones a la nueva aplicación. En Application.scala cambiamos el contenido del response:

def index = Action {
  Ok("Hello world")
}

Con este cambio, el action index responderá un text/plain Hello world. Para ver este cambio solo debemos refrescar la página de nuestro browser::

No necesita compilar el código o reiniciar el servidor para ver la modificación. Este se recargará automáticamente cuando se detecten cambios. ¿Pero qué pasa cuando comete un error en el código? Vamos a intentarlo:

def index = Action {
  Ok("Hello world)
}

Ahora recargue la home page en su browser:

Como puede ver, los errores se muestran directamente en el navegador de una forma amigable.

Preparando la aplicación

Para nuestra aplicación de lista de tareas pendientes, necesitamos algunas actions y las correspondientes URLs. Empecemos por la definición de las direcciones o routes.

Modifique el archivo conf/routes:

# Home page
GET     /                       controllers.Application.index
                                
# Tasks          
GET     /tasks                  controllers.Application.tasks
POST    /tasks                  controllers.Application.newTask
POST    /tasks/:id/delete       controllers.Application.deleteTask(id: Long)

Aquí hemos creado una ruta para listar todas las tareas, y otras que serán utilizadas para su crearlas y eliminarlas. La ruta para eliminarlas incluye una variable id en la URL, este valor es pasado al método deleteTask.

Ahora si refresca el navegador, verá que Play no puede compilar el archivo de rutas:

Esto es porque las rutas hacen referencia a métodos action que aún no existen. Así que vamos a agregarlos al archivo Application.scala:

object Application extends Controller {
  
  def index = Action {
    Ok("Hello world")
  }
  
  def tasks = TODO
  
  def newTask = TODO
  
  def deleteTask(id: Long) = TODO
  
}

Como puede ver retornamos TODO como resultado de la implementación de nuestras actions. Como aún no queremos implementar estas acciones podemos usar el resultado TODO para retornar una respuesta 503 Not Implemented.

Puede intentar acceder a http://localhost:9000/tasks para ver esto en acción:

Ahora lo último que necesitamos arreglar antes de empezar con la implementación de los métodos, es la acción index. Necesitamos que nos redireccione automáticamente a la página que nos muestra la lista de tareas:

def index = Action {
  Redirect(routes.Application.tasks)
}

Como puede ver, usamos Redirect en lugar de Ok para retornar un tipo de respuesta 303 See Other. También hacemos uso de las rutas invertidas para obtener la URL correspondiente a la página que nos muestra la lista de tareas.

Note: Read more about the Router and reverse router.

Preparando el modelo de Tareas Task

Antes de continuar con la implementación necesitamos definir cómo representaremos una tarea, o Task, en nuestra aplicación. Para ello, crearemos una case clase en el archivo app/models/Task.scala:

package models

case class Task(id: Long, label: String)

object Task {
  
  def all(): List[Task] = Nil
  
  def create(label: String) {}
  
  def delete(id: Long) {}
  
}

También hemos creado un companion object para manejar las operaciones que efectuaremos sobre las tareas. Por ahora vamos a escribir implementaciones vacías para cada operación, pero luego en este tutorial desarrollaremos las implementaciones que se encargarán de persistir las tareas en una base de datos relacional.

El template de la aplicación

Nuestra aplicación consistirá en una página muy simple que contendrá tanto la lista de las tareas almacenadas en nuestro sistema, así como un formulario para crear nuevas. Vamos a cambiar el template index.scala.html por:

@(tasks: List[Task], taskForm: Form[String])

@import helper._

@main("Todo list") {
    
    <h1>@tasks.size task(s)</h1>
    
    <ul>
        @tasks.map { task =>
            <li>
                @task.label
                
                @form(routes.Application.deleteTask(task.id)) {
                    <input type="submit" value="Delete">
                }
            </li>
        }
    </ul>
    
    <h2>Add a new task</h2>
    
    @form(routes.Application.newTask) {
        
        @inputText(taskForm("label")) 
        
        <input type="submit" value="Create">
        
    }
    
}

Cambiamos la firma del template para que acepte 2 parámetros:

  • Una lista de tareas a mostrar
  • Un formulario para ingresar nuevas tareas

También importamos helper._ para poder hacer uso de varias funciones que nos ayudarán a trabajar con formularios HTML, en especial la función form que crea el tag HTML <form> con los correspondientes atributos action y method, y la función inputText que crea el input HTML para los campos del formulario.

Nota: Lea más sobre el sistema de templates y las funciones para trabajar con formularios HTML.

El formuario de tareas

Un objeto Form contiene la definición del fomulario HTML, incluyendo las validaciones a realizar sobre los datos. Creemos un simple formulario en nuestro controller Application: tan solo necesitaremos un formulario con un campo label. El formulario también controlará que el label provisto por el usuario no esté vacío:

import play.api.data._
import play.api.data.Forms._

val taskForm = Form(
  "label" -> nonEmptyText
)

El tipo de dato de taskForm es Form[String] ya que es un formulario que nos permite generar un simple String. También necesitará importar algunas clases de play.api.data.

Nota: Lea más acerca de Definición de formularios.

Renderizando la primer página

Ahora contamos con todos los elementos que precisamos para mostrar la página de la aplicación. Vamos a escribir las acciones de las tasks:

def tasks = Action {
  Ok(views.html.index(Task.all(), taskForm))
}

Esto retornará un resultado HTTP 200 OK y como contenido enviará el HTML devuelto por el template index.scala.html llamado con la lista de tareas y el formulario para dar de alta tareas.

Puede intentar acceder a http://localhost:9000/tasks en su navegador:

Manejando el envío del formulario

Por el momento, si ingresamos una nueva tarea, seguiremos obteniendo la página TODO. Vamos a implementar el método newTask:

def newTask = Action { implicit request =>
  taskForm.bindFromRequest.fold(
    errors => BadRequest(views.html.index(Task.all(), errors)),
    label => {
      Task.create(label)
      Redirect(routes.Application.tasks)
    }
  )
}

Para cargar el formulario precisamos tener el request dentro del contexto usado por bindFromRequest para crear un nuevo formulario con los datos de la solicitud. Si ocurriera cualquier error en el formulario, volveremos a mostrar el mismo (retornando un 400 Bar Request en vez de 200 OK) para que el usuario corrija la información. Si no hay errores, creamos la tarea y redireccionamos a la lista de tareas.

Nota: Lea más sobre la Presentación de formularios.

Persistiendo las tareas en base de datos

Para que nuestra aplicación sea de utilidad, debemos persistir las tareas en la base de datos. Empezaremos habilitando una base de datos en nuestra aplicación. En el archivo `conf/application.com, agregamos:

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

Por ahora vamos a usar una simple base de datos H2 en memoria. No necesitamos reiniciar el server, con refrescar el browser será suficiente para iniciar la base de datos.

En este tutorial usaremos Anorm para hacer consultas a la base. Primero tendremos que definir el esquema de la base de datos. Para ello, utilizaremos las "evolutions" de Play, así que crearemos un primer script de evolution en conf/evolutions/default/1.sql:

# Tasks schema
 
# --- !Ups

CREATE SEQUENCE task_id_seq;
CREATE TABLE task (
    id integer NOT NULL DEFAULT nextval('task_id_seq'),
    label varchar(255)
);
 
# --- !Downs
 
DROP TABLE task;
DROP SEQUENCE task_id_seq;

Si refresca su explorador, Play le advertirà que su base de datos necesita ejecutar scripts de evolution:

Haga click en el botón Apply script para ejecutar el script. Ahora su base de datos se encuentra lista.

Nota: Lea mas acerca de Evolutions.

Ahora deberemos implementar los comandos SQL del companion object de la clase Task, comenzando por el método all(). Utilizando Anorm podemos definir un parser que se encargará de transformar cada row del ResultSet de JDBC a un nuevo objeto de tipo Task:

import anorm._
import anorm.SqlParser._

val task = {
  get[Long]("id") ~ 
  get[String]("label") map {
    case id~label => Task(id, label)
  }
}

En este caso, task es un parser que, dada una fila de un ResultSet de JDBC que tenga al menos una columna id y label, es capaz de crear un objeto Task.

Ahora podemos utilizar este parser para implementar el método all():

import play.api.db._
import play.api.Play.current

def all(): List[Task] = DB.withConnection { implicit c =>
  SQL("select * from task").as(task *)
}

Utilizamos DB.withConnection para crear y liberar automáticamente la conexión JDBC.

También utilizamos el método de Anorm SQL para crear la consulta. El método as nos permite procesar el ResultSet utilizando el parser task *: procesará tantas filas como le sea posible, y retornará un List[Task] (dado que nuestro parser task retorna un Task).

Ahora podemos completar la implementación de los demás métodos:

def create(label: String) {
  DB.withConnection { implicit c =>
    SQL("insert into task (label) values ({label})").on(
      'label -> label
    ).executeUpdate()
  }
}

def delete(id: Long) {
  DB.withConnection { implicit c =>
    SQL("delete from task where id = {id}").on(
      'id -> id
    ).executeUpdate()
  }
}

Puede ponerse a probar su aplicación, debería poder crear nuesvas tareas.

Nota: Lea más acerca de Anorm.

Eliminando tareas

Ahora que ya podemos crear tareas, necesitamos poder eliminarlas. Muy simple, solo necesitamos terminar la implementación de la acción deleteTask:

def deleteTask(id: Long) = Action {
  Task.delete(id)
  Redirect(routes.Application.tasks)
}

Desplegando la aplicación en Heroku

Toda la funcionalidad de nuestra aplicación ya está lista. Es hora de ponerla en producción. Despleguémosla en Heroku. Primero tiene que crear un archivo Procfile para Heroku en el directorio raíz de la aplicación:

web: target/start -Dhttp.port=${PORT} -DapplyEvolutions.default=true -Ddb.default.url=${DATABASE_URL} -Ddb.default.driver=org.postgresql.Driver

Nota: Lea mas sobre Desplegar aplicaciones en Heroku.

Usaremos las propiedades de sistema para sobreescribir la configuración de la aplicación cuando esté corriendo en Heroku. Dado que Heroku provee un motor de bases de datos PostgreSQL tendremos que agregar el driver a las dependencias de nuestra aplicación.

Para ello, deberemos agregar al archivo project/Build.scala:

val appDependencies = Seq(
  "postgresql" % "postgresql" % "8.4-702.jdbc4"
)

Nota: Lea mas sobre Manejo de dependencias.

Heroku usa git para poner en producción su aplicación. Vamos a inicializar el repositorio git:

$ git init
$ git add .
$ git commit -m "init"

Ahora podemos crear la aplicación en Heroku:

$ heroku create --stack cedar

Creating warm-frost-1289... done, stack is cedar
http://warm-1289.herokuapp.com/ | [email protected]:warm-1289.git
Git remote heroku added

Y luego pondremos nuestra aplicación en producción con un simple git push heroku master:

$ git push heroku master

Counting objects: 34, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (20/20), done.
Writing objects: 100% (34/34), 35.45 KiB, done.
Total 34 (delta 0), reused 0 (delta 0)

-----> Heroku receiving push
-----> Scala app detected
-----> Building app with sbt v0.11.0
-----> Running: sbt clean compile stage
       ...
-----> Discovering process types
       Procfile declares types -> web
-----> Compiled slug size is 46.3MB
-----> Launching... done, v5
       http://8044.herokuapp.com deployed to Heroku

To [email protected]:floating-lightning-8044.git
* [new branch]      master -> master

Heroku se encargará de hacer el build de nuestra aplicación y la desplegará en un nodo en algún lugar de la nube. Puede verificar el estado del proceso con el siguiente comando:

$ heroku ps

Process       State               Command
------------  ------------------  ----------------------
web.1         up for 10s          target/start

Ya está, ahora puede verla en acción desde el navegador.

¡Su primer aplicación ya está corriendo en producción!

⚠️ **GitHub.com Fallback** ⚠️