Desarrollando un Punto de Venta en Lift 2.6 - (2/4)






Este tutorial consta de varias partes:
  1. Configuración del ambiente
  2. Creación de modelos
  3. Creación de Snippets
  4. Creación de Pantallas

Paso 6 - Arrancamos!

Finalmente, es tiempo de iniciar! Estamos listos para comenzar a desarrollar la aplicación de Punto de Venta.

Paso 7 - Configuración de la Base de Datos

Lo primero que vamos a hacer es configurar la conexión a la base de datos. En este caso de ejemplo, utilizaremos PostgreSQL.

Para ello crearemos el archivo default.props en la siguiente directorio src/main/resources/props. Aquí es donde definiremos los siguientes atributos:

db.driver=org.postgresql.Driver
db.url=jdbc:postgresql://localhost/POS
db.user=pos
db.password=pos112233

Si en este punto, creas la base de datos antes definida, y vuelves a escribir en el ambiente de SBT container:start, podrás comprobar que en la base de datos especificada se ha creado ya la tabla de usuarios.

Aunque parezca un acto de magia la creación de la tabla, es responsabilidad de una aplicación llamada Schemifier.

Paso 8 - Creación de los modelos

En el paso anterior se mencionó el Schemifier. Esta es una herramienta para la creación de tablas y agregar nuevas columnas de manera automática. Para que esta funcione, es necesario crear los modelos.

¿Qué son los modelos? Son clases de Scala, que definen la estructura del objeto que deseamos utilizar en nuestra aplicación. Gracias a otra herramienta, una ORM llamada Mapper, Lift mapea estas clases a la base de datos.

Para comprender esto mejor, comencemos por mostrar un ejemplo de un modelo.

package net.joslash.pos.model

import net.liftweb.mapper._
import net.liftweb.util.FieldError

import net.liftweb.common.Logger

object Client extends Client 
 with LongKeyedMetaMapper[Client]
 with CRUDify[Long, Client]{
 override def dbTableName = "clients"
}

class Client  extends LongKeyedMapper[Client]
 with IdPK
 with CreatedUpdated 
 with OneToMany [Long, Client] 
 with Logger{
  def getSingleton = Client 
  object name extends MappedString(this, 250) {
   override def dbIndexed_? = true
      override def dbNotNull_? = true
      override def required_? = true
      override def validations = 
    valMinLen(3, "El nombre del cliente debe ser de al menos 3 caracteres") _ ::
    valUnique("Este cliente ya fue dado de alta con anterioridad") _ :: 
    super.validations
  }
  object rfc extends MappedString(this, 20) {
   def validateRFCLength (rfc : String) = {
    if(rfc.length < 10){
     List(FieldError(this, "El RFC debe ser al menos de 10 caracteres!"))
    }
    else if(rfc.length > 13){
     List(FieldError(this, "El RFC de ser maximo de 13 caracteres!"))
    }
    else{
     List[FieldError]()
    }
   }
   override def validations = validateRFCLength _ :: 
    valUnique("Un cliente con este RFC ya existe") _ :: super.validations
   override def dbNotNull_? = true
   override def dbIndexed_? = true
      override def required_? = true
  }
  object status extends MappedEnum(this, ItemStatus) {
   override def defaultValue = ItemStatus.ACTIVE
   override def dbNotNull_? = true
  }
  object sales extends MappedOneToMany(Sales, Sales.client,OrderBy(Sales.createdAt, Ascending))
   with Cascade[Sales]
   with Owned[Sales]

  def clientList = {
   Client.findAll(OrderBy(Client.name, Ascending))
  }
} 

Quizá lo primero que llamará tu atención es que existe un object y un class.

De acuerdo a una respuesta dada a esta pregunta en StackOverflow:
"Una clase es una definición en términos de métodos y composición de otros tipos. Un objeto es una instancia de una clase, garantizándose que será única".
Una vez aclarados esto términos, se puede comprender que el object Client implementa la clase Client. Hasta donde nosotros hemos visto, los nombres de ambas estructuras son iguales siempre.

Además de extender Client, el objeto también extiende otras dos clases propias de Lift: LongKeyedMetaMapper y CRUDify. Ambas son necesarias para implementar Mapper (ver link en los siguientes párrafos) en nuestra aplicación.

Dentro de la definición de la clase Client, se puede ver que esta extiende otras clases como IdPK, CreatedUpdated, OneToMany y Logger. Y a continuación se definen los distintos atributos que esta clase tendrá, y que en su momento se verán plasmados en una tabla en la base de datos.

Para comprender mejor para qué se utilizan cada uno de estas clases, te recomendamos leer esta breve documentación de lo que es Mapper y cómo se utiliza.

A breve resumen listamos aquí dichas clases:
  • IdPK, para que se cree una columna Id
  • CreatedUpdated, para guarda automáticamente en la base de datos la fecha de creación del registro, y la fecha de la última modificación del mismo
  • OneToMany, para indicar que este modelo estará relacionado con otro modelo
  • Logger, para poder hacer uso de mensajes de log4j (trace,debug,warn,etc).
  • CRUDify, agrega funcionalidad para altas, bajas, cambios de manera automática
Dentro del cuerpo del class Client, encontrarás  la definición de los atributos propios del Cliente, a saber name, rfc y status. Vamos a ver de manera un poco más detallada la configuración del atributo name a manera de ejemplo.
  • Es de tipo String con una longitud máxima de 250 caracteres. 
  • Se especifica que este atributo se debe indexar.
  • No debe ser null.
  • Es un campo requerido. 
Hasta ahora todo está muy claro, no?.
Lift ofrece un mecanismo de validación muy flexible, que permite garantizar la integridad de los datos que el usuario captura.Ya sabes, si metió un dato incorrecto, o por el contrario no capturó nada donde debía hacerlo, etc.
Como puedes apreciar en el atributo name, la validación de la longitud mínima (valMinLen), así como el garantizar que el nombre sea único (valUnique), se hace de manera muy sencilla.
Por otra parte, el atributo rfc, tiene una forma de validación un poco distinta. Hay ocasiones que se requieren validaciones más complejas, y para ello Lift ofrece la opción de que se defina una función propia donde se puedan plasmar todas las distintas opciones que sea necesario considerar. Es importante hacer notar, que dicha función se debe añadir a validations, tal cual se hizo previamente con valUnique y valMinLen.

El atributo status, se define como un enum, en este caso llamado ItemStatus. A continuación puedes ver el código que lo declara.
object ItemStatus extends Enumeration {
 val ACTIVE = Value(1,"Activo")
 val CANCELED = Value(2,"Cancelado")
}

En Scala es posible declarar dentro de un mismo archivo diversos objects. Así que ItemStatus lo encontrarás declarado al inicio de la clase Item. A continuación puedes ver dicha clase en su totalidad.

package net.joslash.pos.model

import net.liftweb.mapper._
import scala.xml.NodeSeq
import _root_.java.math.MathContext
import net.liftweb.sitemap.Loc.LocGroup

import net.liftweb.common.Logger

object ItemStatus extends Enumeration {
 val ACTIVE = Value(1,"Activo")
 val CANCELED = Value(2,"Cancelado")
}

object Item extends Item 
 with LongKeyedMetaMapper[Item]
 with CRUDify[Long, Item]{
 
 override def dbTableName = "items"
}

class Item extends LongKeyedMapper[Item]
 with IdPK
 with CreatedUpdated 
 with OneToMany[Long, Item] 
 with Logger{
  def getSingleton = Item
  object name extends MappedString(this, 200) {
   override def dbIndexed_? = true
   override def dbNotNull_? = true
   override def required_? = true
   override def validations = 
    valMinLen(3, "El nombre del artículo debe ser de al menos 3 caracteres") _ ::
    valUnique("Este articulo ya fue dado de alta con anterioridad") _ :: 
    super.validations
  }
  object price extends MappedDecimal(this, MathContext.DECIMAL64, 3) {
   override def dbNotNull_? = true
   override def required_? = true
  }
  object status extends MappedEnum(this, ItemStatus) {
   override def defaultValue = ItemStatus.ACTIVE
   override def dbNotNull_? = true
  }
  object stock extends MappedOneToMany(Stock, Stock.item,OrderBy(Stock.item, Ascending))
   with Cascade[Stock]
   with Owned[Stock] 

  def itemList = {
   Item.findAll(OrderBy(Item.name, Ascending))
  }
}

Observando de cerca esta segunda clase, es fácil notar la similitud con la clase Client. Un diferencia que salta a la vista, es la definición del atributo price, el cual es tipo Decimal.

De acuerdo a nuestra experiencia, siempre que tengas atributos que vayan  a guardar cantidades que representan dinero, utiliza el tipo BigDecimal.

Recuerdas que al inicio se hizo mención de que Lift requiere la JVM para poder funcionar? Bueno, pues esto abre la puerta para poder utilizar clases de Java dentro de Lift. Al parecer, una vez se compila la aplicación de Lift, todo es traducido a bytecodes, al igual que cualquier aplicación de Java.

En este caso, MathContext es una clase de Java. Y la manera correcta de importarla es _root_.java.math.MathContext. Es fácil apreciar que sólo se agregó _root_. al paquete de Java.

No podemos dejar fuera las relaciones entre los distintos modelos. En este caso en particular, Client se relaciona con el modelo Sales.
Es una relación uno a muchos, y se le indica el atributo de la clase Sales que será la llave foránea, y cómo queremos que ordene los listados de dicha relación. Se agrega también la caracterísitca de cascada para cuidar la integridad de los datos.

object sales extends MappedOneToMany(Sales, Sales.client,OrderBy(Sales.createdAt, Ascending))
   with Cascade[Sales]
   with Owned[Sales]

Finalmente,  para hacer más legible el código, agregamos un método que regreas todos los clientes ordenados por nombre. Podríamos tener muchos métodos más, tantos como requieras.

Si deseas ver todos los modelos definidos en esta aplicación, en esta liga los puedes consultar.

Paso 9 - Schemifier

Una vez definidos todos tus modelos, puedes dejar en manos del Schemifier la creación de las tablas y todo lo relacionado con la base de datos.

Para ello basta con abrir el archivo Boot.scala, donde encontrarás diversas instrucciones de configuración de Lift. Este archivo se lee cuando se levanta el ambiente y la aplicación.

Por ejemplo, aquí está definida la configuración de conexión a la base de datos.
if (!DB.jndiJdbcConnAvailable_?) {
      val vendor = 
 new StandardDBVendor(Props.get("db.driver") openOr "org.h2.Driver",
        Props.get("db.url") openOr 
        "jdbc:h2:lift_proto.db;AUTO_SERVER=TRUE",
        Props.get("db.user"), Props.get("db.password"))

      LiftRules.unloadHooks.append(vendor.closeAllConnections_! _)

      DB.defineConnectionManager(util.DefaultConnectionIdentifier, vendor)
    }

Recuerdas que se definieron los parámetros correspondientes en el archivo default.props? Bueno, es en este lugar donde se reemplazan de manera automática y se conecta a la base de datos. Por el momento, concentrémonos en el Schemifier. Justo al finalizar el código de conexión a la base de datos, se encuentra definido esta herramienta. Todo lo que hay que hacer es agregar después de User, los nombres de los modelos que hemos creado, y agregar el import correspondiente al paquete model donde están todas las clases.
import net.joslash.pos.model._
...
Schemifier.schemify(true, Schemifier.infoF _, User, Item, Stock, Client, Sales)

Comentarios

Entradas populares de este blog

Batch File como Servicio de Windows

Cómo crear archivos XML en Java con JAXB

Ejecutando Jetty como un Servicio en Windows Server 2012