Skip to content

18. Приклад із практики: Struts 2 / Tiles / Spring / Hibernate / MySQL

Ми завершимо наше вивчення Struts аналізом конкретного випадку. Щоб приклад був реалістичним, він буде значно складнішим за ті, що ми розглядали раніше. Початківцям, безсумнівно, краще поглибити знання основ Struts 2 на власних додатках, перш ніж переходити до цього аналізу конкретного випадку.

Додаток використовуватиме багаторівневу архітектуру:

Набір шарів [metier], [dao], [jpa/hibernate] буде надано нам у вигляді архіву jar, функціональні можливості якого ми детально розглянемо. Інтеграцію шарів забезпечить Spring. Шар [web] буде реалізовано за допомогою Struts 2.

18.1. Задача

Ми пропонуємо створити веб-додаток, що дозволяє формувати розрахункові листки для виховательок, які працюють у «Дому раннього дитинства» однієї з громад.

Image

Цей приклад з практики представлено в документі:

«Вступ до Java» EE 5, доступний за посиланням URL [http://tahe.developpez.com/java/javaee]

У цьому документі приклад реалізації реалізовано з використанням такої багатошарової архітектури:

Рівень [web] реалізовано за допомогою фреймворку JSF (Java Server Faces). Ми використаємо цю саму архітектуру, реалізувавши рівень [web] за допомогою Struts 2. Щоб продемонструвати переваги багатошарової архітектури, ми використаємо архів jar шарів [metier, dao, jpa] з версії JSF і підключимо його до шару [web / struts2]:

Ми розглянемо такі елементи шару [metier, dao, jpa]:

  • шар [web] взаємодіє з інтерфейсом шару [métier]. Ми розглянемо цей інтерфейс.
  • шар [jpa] здійснює доступ до бази даних. Ми розглянемо його.
  • Рівень [jpa] перетворює рядки таблиць бази даних на сутності JPA, з якими працюють усі рівні додатка. Ми їх розглянемо.
  • Рівні [metier, dao, jpa] інстанціюються Spring. Ми розглянемо файл конфігурації, який забезпечує цю інстанціювання та інтеграцію.

18.2. База даних

Ми будемо використовувати таку базу даних MySQL [dbpam_hibernate]:

Image

  • У [1] база даних містить три таблиці:
    • [employes]: таблиця, що містить дані про працівниць дитячого садка
    • [cotisations]: таблиця, що містить ставки соціальних внесків
    • [indemnites]: таблиця, що містить інформацію для розрахунку заробітної плати працівниць

Таблиця [employes]

  • у [2] — таблиця співробітників, а в [3] — значення її полів

Вміст таблиці може бути таким:

 

Таблиця [cotisations]

  • у [4] — таблиця внесків, а в [5] — значення її полів

Зміст таблиці може бути таким:

 

Таблиця [indemnites]

  • у [6] — таблиця компенсацій, а в [7] — значення її полів

Зміст таблиці може бути таким:

 

Експорт структури бази даних у файл SQL дає такий результат:

#
# Структура таблиці `cotisations`: 
#

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;

#
# Структура таблиці `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;

#
# Структура таблиці `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. Елементи JPA

У наступній архітектурі

Шар [Jpa] виконує роль мосту між об’єктами, з якими працює шар [dao], та рядками таблиць бази даних, з якими працює драйвер JDBC. Рядки таблиць, зчитані з бази даних, перетворюються на об’єкти, які називаються сутностями JPA. І навпаки, під час запису сутності JPA перетворюються на рядки в таблицях. Ці сутності обробляються всіма рівнями, зокрема веб-рівнем. Тому нам необхідно їх знати:

Ентітет [Employe] представляє рядок таблиці [Employes]

ID
первинний ключ типу autoincrement
VERSION
номер версії запису
PRENOM
ім'я працівниці
NOM
її прізвище
ADRESSE
її адреса
CP
його поштовий індекс
VILLE
його місто
INDEMNITE_ID
зовнішній ключ на INDEMNITES(ID)

Об'єкт [Employe] має такий вигляд:


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()
    +"]";
  }
  
  // методи отримання та встановлення значень
   ...  
}

Анотації @, призначені для шару [Jpa], ігноруються. Різні поля класу відповідають різним стовпцям таблиці [EMPLOYES]. Поле «indemnites» (рядок 29) відображає те, що таблиця [EMPLOYES] має зовнішній ключ до таблиці [INDEMNITES]. При обробці даних про співробітника одночасно обробляються й дані про його надбавки.

Об’єкт [Indemnite] є об’єктним виразом одного рядка таблиці [INDEMNITES]:

ID
первинний ключ типу автоінкременту
VERSION
номер версії запису
BASE_HEURE
вартість однієї години чергування в євро
ENTRETIEN_JOUR
добова надбавка за чергування в євро
REPAS_JOUR
допомога на харчування в євро за день чергування
INDEMNITES_CP
допомога за оплачувану відпустку. Це відсоток
, що застосовується до базової заробітної плати.

Елемент [Indemnite] має такий вигляд:


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()
    + "]";
  }
  
   // методи отримання та встановлення значень
....  
}

Різні поля класу відображають різні стовпці таблиці [INDEMNITES].

Ентітет [Cotisation] є об’єктним виразом одного рядка таблиці [COTISATIONS]:

ID
первинний ключ типу autoincrement
VERSION
номер версії запису
SECU
ставка (у відсотках) страхового внеску
соціального страхування
RETRAITE
Ставка внеску на пенсійне забезпечення
CSGD
ставка внеску на загальний соціальний внесок
загальні відрахування, що підлягають вирахуванню
CSGRDS
Ставка внеску на загальний соціальний внесок
та внесок на погашення
соціального боргу

Елемент [Cotisation] має такий вигляд:


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()+"]";
  }
  
  // методи отримання та встановлення значень
  ... 
}

Різні поля класу відповідають різним стовпцям таблиці [INDEMNITES].

18.4. Спосіб розрахунку заробітної плати няні

Веб-додаток, який ми будемо розробляти, дозволить нам розраховувати заробітну плату працівниці на основі трьох даних:

  • індекс працівниці
  • кількість відпрацьованих днів
  • кількість відпрацьованих годин

Ось знімок екрана з розрахунком заробітної плати:

Image

Тепер ми розглянемо спосіб розрахунку місячної заробітної плати няні. Він не претендує на те, щоб відповідати реальному. Як приклад візьмемо заробітну плату пані Марі Жувенал, яка пропрацювала 150 годин протягом 20 днів у місяці, за який виплачується заробітна плата.

Враховуються такі елементи:
[TOTALHEURES]: total des heures
 travaillées dans le mois
[TOTALJOURS]: total des jours
 travaillés dans le mois
[TOTALHEURES]=150
[TOTALJOURS]= 20
Базова заробітна плата виховательки
визначається за такою формулою
:
[SALAIREBASE]=([TOTALHEURES]
*[BASEHEURE])*(1+[INDEMNITESCP]/
100)
[SALAIREBASE]=(150*[2.1])
*(1+0.15)= 362,25
Певна сума соціальних внесків
мають бути утримані з цієї базової заробітної плати
:
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
Загальна сума соціальних внесків:
[COTISATIONSSOCIALES]=[SALAIREBASE]
*(CSGRDS+CSGD+SECU+RETRAITE)/100
[COTISATIONSSOCIALES]=97,48
Крім того, вихователька має право,
за кожен відпрацьований день на компенсацію
на утримання, а також на компенсацію
харчування. З цієї причини вона отримує
:
[Indemnités]=[TOTALJOURS]
*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104
У підсумку чиста заробітна плата, що підлягає виплаті няні, становить:
[SALAIREBASE]-
[COTISATIONSSOCIALES]+[INDEMNITÉS]
[salaire NET]=368,77

18.5. Інтерфейс шару [métier]

Повернемося до архітектури додатка, який ми створюємо:

Рівень [web / struts 2] взаємодіє з інтерфейсом рівня [métier]. Він має такий вигляд:


package metier;

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

public interface IMetier {
  // отримати платіжну відомість
  FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
  // список співробітників
  List<Employe> findAllEmployes();
}
  • рядок 8: метод, який дозволить нам обчислити зарплату співробітника
  • рядок 10: метод, який дозволить нам заповнити комбінований список співробітників

Метод calculerFeuillesalaire повертає екземпляр класу [FeuilleSalaire] наступного вигляду:


package metier;

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

public class FeuilleSalaire implements Serializable {
  // приватні поля

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

  // конструктори
  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 + "]";
  }

  // методи getter та setter
  ...
}

Табель оплати праці містить таку інформацію:

  • рядок 10: інформація про працівника, для якого розраховується заробітна плата
  • рядок 11: різні ставки відрахувань
  • рядок 12: складові заробітної плати

Клас [ElementsSalaire] має такий вигляд:


package metier;

import java.io.Serializable;

public class ElementsSalaire implements Serializable{
  
  // приватні поля
  private double salaireBase;
  private double cotisationsSociales;
  private double indemnitesEntretien;
  private double indemnitesRepas;
  private double salaireNet;
  
  // конструктори
  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 + "]";
  }
  
  // геттери та сеттери
  ...
}
  • рядки 8–12: складові заробітної плати

18.6. Конфігураційний файл Spring

Інтеграція шарів [métier, dao, jpa] забезпечується таким конфігураційним файлом 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">
  
  <!-- прикладні рівні -->
 
<!-- бізнес-логіка -->
  <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" />
  
  <!-- конфігурація 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>
  
  <!-- джерело даних 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>
  
  <!-- менеджер транзакцій -->
  <tx:annotation-driven transaction-manager="txManager" />
  <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
  </bean>
  
  <!-- перетворення винятків -->
  <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
  
  <!-- збереження даних -->
  <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
  
</beans>

Ми не будемо намагатися пояснити цю конфігурацію. Вона необхідна для інстанціювання та інтеграції шарів [métier, dao, jpa]. Отже, наш веб-додаток, який буде базуватися на цих шарах, повинен використовувати цю конфігурацію. Зазначимо, що рядки 32–37 визначають параметри Jdbc для бази даних. Читач, який бажає змінити базу даних, повинен відредагувати ці рядки.

Для отримання додаткової інформації щодо цієї конфігурації слід ознайомитися з документом «Вступ до Java EE 5», доступним на URL [http://tahe.developpez.com/java/javaee].