Skip to content

18. Caso de estudio: Struts 2 / Tiles / Spring / Hibernate / MySQL

Terminaremos nuestro aprendizaje de Struts con un caso de estudio. Para que sea realista, el ejemplo que analizaremos será mucho más complejo que los anteriores. Para los principiantes, sin duda es mejor profundizar en los conceptos básicos de Struts 2 con aplicaciones personales antes de abordar este caso de estudio.

La aplicación utilizará una arquitectura por capas:

El conjunto de capas [metier], [dao] y [jpa/hibernate] se nos proporcionará en forma de un archivo JAR, cuyas funcionalidades detallaremos. La integración de las capas estará a cargo de Spring. La capa [web] se implementará mediante Struts 2.

18.1. El problema

Nos proponemos desarrollar una aplicación web que permita generar la nómina de las cuidadoras infantiles empleadas por la «Casa de la Primera Infancia» de un municipio.

Image

Este caso de estudio se presenta en el documento:

Introducción a Java EE 5, disponible en URL [http://tahe.developpez.com/java/javaee]

En este documento, el caso de estudio se implementa con la siguiente arquitectura multicapa:

La capa [web] se implementa utilizando el marco JSF (Java Server Faces). Vamos a adoptar esta misma arquitectura al implementar la capa [web] con Struts 2. Para demostrar las ventajas de las arquitecturas en capas, utilizaremos el archivo jar de las capas [metier, dao, jpa] de la versión JSF y lo conectaremos a una capa [web / struts2]:

Presentaremos los siguientes elementos de las capas [metier, dao, jpa]:

  • la capa [web] se comunica con la interfaz de la capa [métier]. Presentaremos esta interfaz.
  • La capa [jpa] accede a una base de datos. La presentaremos.
  • La capa [jpa] transforma las filas de las tablas de la base de datos en entidades JPA que son manipuladas por todas las capas de la aplicación. Las presentaremos.
  • Las capas [metier, dao, jpa] son instanciadas por Spring. Presentaremos el archivo de configuración que lleva a cabo esta instanciación e integración.

18.2. La base de datos

Utilizaremos la siguiente base de datos MySQL [dbpam_hibernate]:

Image

  • En [1], la base de datos cuenta con tres tablas:
    • [employes]: una tabla que registra a las empleadas de una guardería
    • [cotisations]: una tabla que registra las tasas de cotizaciones sociales
    • [indemnites]: una tabla que registra la información necesaria para calcular el salario de las empleadas

Tabla [employes]

  • en [2], la tabla de empleados, y en [3], el significado de sus campos

El contenido de la tabla podría ser el siguiente:

 

Tabla [cotisations]

  • en [4], la tabla de cotizaciones y en [5], el significado de sus campos

El contenido de la tabla podría ser el siguiente:

 

Tabla [indemnites]

  • en [6], la tabla de indemnizaciones y en [7], el significado de sus campos

El contenido de la tabla podría ser el siguiente:

 

La exportación de la estructura de la base de datos a un archivo SQL da el siguiente resultado:

#
# Estructura de la tabla `cotizaciones`: 
#

CREATE TABLE `cotisations` (
  `ID` bigint(20) NOT NULL auto_increment,
  `SECU` double NOT NULL,
  `RETRAITE` double NOT NULL,
  `CSGD` double NOT NULL,
  `CSGRDS` double NOT NULL,
  `VERSION` int(11) NOT NULL,
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;

#
# Estructura de la tabla `indemnites`: 
#

CREATE TABLE `indemnites` (
  `ID` bigint(20) NOT NULL auto_increment,
  `ENTRETIEN_JOUR` double NOT NULL,
  `REPAS_JOUR` double NOT NULL,
  `INDICE` int(11) NOT NULL,
  `INDEMNITES_CP` double NOT NULL,
  `BASE_HEURE` double NOT NULL,
  `VERSION` int(11) NOT NULL,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `INDICE` (`INDICE`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;

#
# Estructura de la tabla `employes`: 
#

CREATE TABLE `employes` (
  `ID` bigint(20) NOT NULL auto_increment,
  `PRENOM` varchar(20) NOT NULL,
  `SS` varchar(15) NOT NULL,
  `ADRESSE` varchar(50) NOT NULL,
  `CP` varchar(5) NOT NULL,
  `VILLE` varchar(30) NOT NULL,
  `NOM` varchar(30) NOT NULL,
  `VERSION` int(11) NOT NULL,
  `INDEMNITE_ID` bigint(20) NOT NULL,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `SS` (`SS`),
  KEY `FK_EMPLOYES_INDEMNITE_ID` (`INDEMNITE_ID`),
  CONSTRAINT `FK_EMPLOYES_INDEMNITE_ID` FOREIGN KEY (`INDEMNITE_ID`) REFERENCES `indemnites` (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1;

18.3. Las entidades JPA

En la siguiente arquitectura

La capa [Jpa] actúa como puente entre los objetos manejados por la capa [dao] y las filas de las tablas de la base de datos manejadas por el controlador JDBC. Las filas de las tablas leídas de la base de datos se transforman en objetos denominados entidades JPA. Por el contrario, al escribir, las entidades JPA se transforman en filas de las tablas. Estas entidades son manejadas por todas las capas y, en particular, por la capa web. Por lo tanto, debemos conocerlas:

La entidad [Employe] representa una fila de la tabla [Employes]

ID
clave primaria de tipo autoincremento
VERSION
N.º de versión del registro
PRENOM
nombre de la empleada
NOM
su apellido
ADRESSE
su dirección
CP
su código postal
VILLE
su ciudad
INDEMNITE_ID
clave externa en INDEMNITES (ID)

La entidad [Employe] es la siguiente:


package jpa;

...

@Entity
@Table(name="EMPLOYES")
public class Employe implements Serializable {
  
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  @Version
  @Column(name="VERSION",nullable=false)
  private int version;
  @Column(name="SS", nullable=false, unique=true, length=15)
  private String SS;
  @Column(name="NOM", nullable=false, length=30)
  private String nom;
  @Column(name="PRENOM", nullable=false, length=20)
  private String prenom;
  @Column(name="ADRESSE", nullable=false, length=50)
  private String adresse;
  @Column(name="VILLE", nullable=false, length=30)
  private String ville;
  @Column(name="CP", nullable=false, length=5)
  private String codePostal;
  @ManyToOne
  @JoinColumn(name="INDEMNITE_ID",nullable=false)
  private Indemnite indemnite;
  
  
  public Employe() {
  }
  
  public Employe(String SS, String nom, String prenom, String adresse, String ville, String codePostal, Indemnite indemnite){
    setSS(SS);
    setNom(nom);
    setPrenom(prenom);
    setAdresse(adresse);
    setVille(ville);
    setCodePostal(codePostal);
    setIndemnite(indemnite);
  }
  
  @Override
  public String toString() {
    return "jpa.Employe[id=" + getId()
    + ",version="+getVersion()
    +",SS="+getSS()
    + ",nom="+getNom()
    + ",prenom="+getPrenom()
    + ",adresse="+getAdresse()
    +",ville="+getVille()
    +",code postal="+getCodePostal()
    +",indice="+getIndemnite().getIndice()
    +"]";
  }
  
  // Métodos de lectura y escritura
   ...  
}

Se ignorarán las anotaciones @ destinadas a la capa [Jpa]. Los distintos campos de la clase reflejan las distintas columnas de la tabla [EMPLOYES]. El campo «indemnites» (línea 29) refleja el hecho de que la tabla [EMPLOYES] tiene una clave externa en la tabla [INDEMNITES]. Al modificar un empleado, también se modifican sus indemnizaciones.

La entidad [Indemnite] es la expresión objeto de una fila de la tabla [INDEMNITES]:

ID
clave primaria de tipo autoincremento
VERSION
N.º de versión del registro
BASE_HEURE
costo en euros de una hora de guardia
ENTRETIEN_JOUR
indemnización en euros por día de guardia
REPAS_JOUR
indemnización por comida en euros por día de guardia
INDEMNITES_CP
indemnizaciones por vacaciones pagadas. Se trata de un porcentaje
que se aplica al salario base.

La entidad [Indemnite] es la siguiente:


package jpa;

...

@Entity
@Table(name="INDEMNITES")
public class Indemnite implements Serializable {
  
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  @Version
  @Column(name="VERSION",nullable=false)
  private int version;
  @Column(name="INDICE", nullable=false,unique=true)
  private int indice;
  @Column(name="BASE_HEURE",nullable=false)
  private double baseHeure;
  @Column(name="ENTRETIEN_JOUR",nullable=false)
  private double entretienJour;
  @Column(name="REPAS_JOUR",nullable=false)
  private double repasJour;
  @Column(name="INDEMNITES_CP",nullable=false)
  private double indemnitesCP;
  
  public Indemnite() {
  }
  
  public Indemnite(int indice, double baseHeure, double entretienJour, double repasJour, double indemnitesCP){
    setIndice(indice);
    setBaseHeure(baseHeure);
    setEntretienJour(entretienJour);
    setRepasJour(repasJour);
    setIndemnitesCP(indemnitesCP);
  }
  
  @Override
  public String toString() {
    return "jpa.Indemnite[id=" + getId()
    + ",version="+getVersion()
    +",indice="+getIndice()
    +",base heure="+getBaseHeure()
    +",entretien jour"+getEntretienJour()
    +",repas jour="+getRepasJour()
    +",indemnités CP="+getIndemnitesCP()
    + "]";
  }
  
   // Métodos de lectura y escritura
....  
}

Los distintos campos de la clase reflejan las distintas columnas de la tabla [INDEMNITES].

La entidad [Cotisation] es la expresión objeto de una fila de la tabla [COTISATIONS]:

ID
clave primaria de tipo autoincremento
VERSION
N.º de versión del registro
SECU
tasa (porcentaje) de cotización para la seguridad
social
RETRAITE
tasa de cotización para la jubilación
CSGD
tasa de cotización para la contribución social
deducible generalizada
CSGRDS
tasa de cotización para la contribución social
generalizada y la contribución para el reembolso
de la deuda social

La entidad [Cotisation] es la siguiente:


package jpa;

...

@Entity
@Table(name="COTISATIONS")
public class Cotisation implements Serializable {
  
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  @Version
  @Column(name="VERSION",nullable=false)
  private int version;
  @Column(name="CSGRDS",nullable=false)
  private double csgrds;
  @Column(name="CSGD",nullable=false)
  private double csgd;
  @Column(name="SECU",nullable=false)
  private double secu;
  @Column(name="RETRAITE",nullable=false)
  private double retraite;
  
  public Cotisation() {
  }
  
  public Cotisation(double csgrds, double csgd, double secu, double retraite){
    setCsgrds(csgrds);
    setCsgd(csgd);
    setSecu(secu);
    setRetraite(retraite);
  }
  
  @Override
  public String toString() {
    return "jpa.Cotisation[id=" + getId() + ",version=" + getVersion()+",csgrds="+getCsgrds()+"" +
      ",csgd="+getCsgd()+",secu="+getSecu()+",retraite="+getRetraite()+"]";
  }
  
  // Métodos de lectura y escritura
  ... 
}

Los distintos campos de la clase reflejan las distintas columnas de la tabla [INDEMNITES].

18.4. Método de cálculo del salario de una cuidadora infantil

La aplicación web que vamos a desarrollar nos permitirá calcular el salario de una empleada a partir de tres datos:

  • el índice de la empleada
  • el número de días trabajados
  • el número de horas trabajadas

Aquí hay una captura de pantalla del cálculo de un salario:

Image

A continuación, presentamos el método de cálculo del salario mensual de una cuidadora infantil. No pretende ser el que se utiliza en la realidad. Tomamos como ejemplo el salario de la Sra. Marie Jouveinal, quien trabajó 150 horas durante 20 días en el mes a pagar.

Se toman en cuenta los siguientes elementos:
[TOTALHEURES]: total des heures
 travaillées dans le mois
[TOTALJOURS]: total des jours
 travaillés dans le mois
[TOTALHEURES]=150
[TOTALJOURS]= 20
El salario base de la asistente
de guardería se calcula mediante la fórmula
siguiente:
[SALAIREBASE]=([TOTALHEURES]
*[BASEHEURE])*(1+[INDEMNITESCP]/
100)
[SALAIREBASE]=(150*[2.1])
*(1+0.15)= 362,25
Se deben deducir ciertas cotizaciones sociales
deben deducirse de este salario
base:
Contribution sociale généralisée
 et contribution au remboursement
 de la dette sociale :
 [SALAIREBASE]*[CSGRDS/100]
Contribution sociale généralisée
 déductible :
 [SALAIREBASE]*[CSGD/100]
Sécurité sociale, veuvage,
 vieillesse :
 [SALAIREBASE]*[SECU/100]
Retraite Complémentaire + AGPF + 
Assurance Chômage : [SALAIREBASE]*[RETRAITE/100]
CSGRDS : 12,64
CSGD : 22,28
Sécurité sociale : 34,02
Retraite : 28,55
Total de contribuciones sociales:
[COTISATIONSSOCIALES]=[SALAIREBASE]
*(CSGRDS+CSGD+SECU+RETRAITE)/100
[COTISATIONSSOCIALES]=97,48
Además, la cuidadora infantil tiene derecho,
por cada día trabajado, a una indemnización
de manutención, así como a una indemnización por
comida. Por este concepto, recibe las indemnizaciones
siguientes:
[Indemnités]=[TOTALJOURS]
*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104
En definitiva, el salario neto que se le debe pagar a la cuidadora infantil es el siguiente:
[SALAIREBASE]-
[COTISATIONSSOCIALES]+[INDEMNITÉS]
[salaire NET]=368,77

18.5. La interfaz de la capa [métier]

Volvamos a la arquitectura de la aplicación que estamos desarrollando:

La capa [web / struts 2] se comunica con la interfaz de la capa [métier]. Esta es la siguiente:


package metier;

import java.util.List;
import jpa.Employe;

public interface IMetier {
  // obtener la nómina
  FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
  // lista de empleados
  List<Employe> findAllEmployes();
}
  • línea 8: el método que nos permitirá calcular el salario de un empleado
  • línea 10: el método que nos permitirá llenar el menú desplegable de empleados

El método calculerFeuillesalaire devuelve una instancia de la clase [FeuilleSalaire] como sigue:


package metier;

import java.io.Serializable;
import jpa.Cotisation;
import jpa.Employe;

public class FeuilleSalaire implements Serializable {
  // campos privados

  private Employe employe;
  private Cotisation cotisation;
  private ElementsSalaire elementsSalaire;

  // constructores
  public FeuilleSalaire() {
  }

  public FeuilleSalaire(Employe employe, Cotisation cotisation,
          ElementsSalaire elementsSalaire) {
    setEmploye(employe);
    setCotisation(cotisation);
    setElementsSalaire(elementsSalaire);
  }

  // toString
  @Override
  public String toString() {
    return "[" + employe + "," + cotisation + ","
            + elementsSalaire + "]";
  }

  // getters y setters
  ...
}

La hoja de nómina contiene la siguiente información:

  • línea 10: información sobre el empleado cuyo salario se está calculando
  • línea 11: las diferentes tasas de cotización
  • línea 12: elementos del salario

La clase [ElementsSalaire] es la siguiente:


package metier;

import java.io.Serializable;

public class ElementsSalaire implements Serializable{
  
  // campos privados
  private double salaireBase;
  private double cotisationsSociales;
  private double indemnitesEntretien;
  private double indemnitesRepas;
  private double salaireNet;
  
  // constructores
  public ElementsSalaire() {
    
  }
  
  public ElementsSalaire(double salaireBase, double cotisationsSociales,
    double indemnitesEntretien, double indemnitesRepas,
    double salaireNet) {
    setSalaireBase(salaireBase);
    setCotisationsSociales(cotisationsSociales);
    setIndemnitesEntretien(indemnitesEntretien);
    setIndemnitesRepas(indemnitesRepas);
    setSalaireNet(salaireNet);
  }
  
  // toString
  @Override
  public String toString() {
    return "[salaire base=" + salaireBase + ",cotisations sociales=" + cotisationsSociales + ",indemnités d'entretien="
      + indemnitesEntretien + ",indemnités de repas=" + indemnitesRepas + ",salaire net="
      + salaireNet + "]";
  }
  
  // getters y setters
  ...
}
  • líneas 8-12: los componentes del salario

18.6. El archivo de configuración de Spring

La integración de las capas [métier, dao, jpa] se lleva a cabo mediante el siguiente archivo de configuración de Spring:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
  
  <!-- capas de aplicación -->
 
<!-- negocio -->
  <bean id="metier" class="metier.Metier">
    <property name="employeDao" ref="employeDao"/>
    <property name="cotisationDao" ref="cotisationDao"/>  
  </bean>
  <!-- DAO -->
  <bean id="employeDao" class="dao.EmployeDao" />
  <bean id="indemniteDao" class="dao.IndemniteDao" />
  <bean id="cotisationDao" class="dao.CotisationDao" />
  
  <!-- configuración JPA -->
  <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="jpaVendorAdapter">
      <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
        <property name="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
      </bean>
    </property>
    <property name="loadTimeWeaver">
      <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    </property>
  </bean>
  
  <!-- la fuente de datos DBCP -->
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="URL" value="jdbc:mysql://localhost:3306/dbpam_hibernate" />
    <property name="username" value="root" />
    <property name="password" value="" />
  </bean>
  
  <!-- el administrador de transacciones -->
  <tx:annotation-driven transaction-manager="txManager" />
  <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
  </bean>
  
  <!-- traducción de excepciones -->
  <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
  
  <!-- persistencia -->
  <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
  
</beans>

No intentaremos explicar esta configuración. Es necesaria para la instanciación e integración de las capas [métier, dao, jpa]. Por lo tanto, nuestra aplicación web, que se basará en estas capas, deberá adoptar esta configuración. Cabe señalar que las líneas 32 a 37 configuran las características JDBC de la base de datos. El lector que desee cambiar de base de datos deberá modificar estas líneas.

Para obtener más información sobre esta configuración, consulte el documento Introducción a Java EE 5, disponible en URL [http://tahe.developpez.com/java/javaee].