Skip to content

18. Studium przypadku: Struts 2 / Tiles / Spring / Hibernate / MySQL

Naszą naukę Struts zakończymy studium przypadku. Aby zapewnić realizm, omawiany przykład będzie znacznie bardziej złożony niż te, które analizowaliśmy wcześniej. Dla początkujących z pewnością lepiej będzie pogłębić podstawy Struts 2 na własnych aplikacjach, zanim przystąpią do tego studium przypadku.

Aplikacja będzie wykorzystywać architekturę warstwową:

Zestaw warstw [metier], [dao], [jpa/hibernate] zostanie nam dostarczony w postaci archiwum jar, którego funkcjonalność omówimy szczegółowo. Integrację warstw zapewni biblioteka Spring. Warstwa [web] zostanie zaimplementowana przy użyciu Struts 2.

18.1. Problem

Zamierzamy stworzyć aplikację internetową umożliwiającą sporządzanie odcinków wypłaty dla opiekunek zatrudnionych przez „Dom Małego Dziecka” w danej gminie.

Image

Niniejsze studium przypadku zostało przedstawione w dokumencie:

Wprowadzenie do języka Java EE 5 dostępnym pod adresem URL [http://tahe.developpez.com/java/javaee]

W niniejszym dokumencie studium przypadku zostało zaimplementowane przy użyciu następującej architektury wielowarstwowej:

Warstwa [web] została zaimplementowana przy użyciu frameworka JSF (Java Server Faces). Wykorzystamy tę samą architekturę, implementując warstwę [web] przy użyciu Struts 2. Aby pokazać zalety architektury warstwowej, wykorzystamy archiwum jar warstw [metier, dao, jpa] z wersji JSF i połączymy je z warstwą [web / struts2]:

Przedstawimy następujące elementy warstw [metier, dao, jpa]:

  • warstwa [web] komunikuje się z interfejsem warstwy [métier]. Przedstawimy ten interfejs.
  • warstwa [jpa] uzyskuje dostęp do bazy danych. Przedstawimy ją.
  • warstwa [jpa] przekształca wiersze tabel bazy danych w encje JPA, z którymi operują wszystkie warstwy aplikacji. Przedstawimy je.
  • Warstwy [metier, dao, jpa] są instancjonowane przez Spring. Przedstawimy plik konfiguracyjny, który realizuje to instancjonowanie i integrację.

18.2. Baza danych

Będziemy korzystać z następującej bazy danych MySQL [dbpam_hibernate]:

Image

  • W bazie [1] znajdują się trzy tabele:
    • [employes]: tabela rejestrująca pracownice żłobka
    • [cotisations]: tabela zawierająca stawki składek na ubezpieczenie społeczne
    • [indemnites]: tabela zawierająca informacje umożliwiające obliczenie wynagrodzenia pracownic

Tabela [employes]

  • w [2] znajduje się tabela pracowników, a w [3] – znaczenie jej pól

Zawartość tabeli mogłaby wyglądać następująco:

 

Tabela [cotisations]

  • w [4] – tabela składek, a w [5] – znaczenie jej pól

Zawartość tabeli mogłaby wyglądać następująco:

 

Tabela [indemnites]

  • w [6] – tabela świadczeń, a w [7] – znaczenie jej pól

Zawartość tabeli mogłaby wyglądać następująco:

 

Eksport struktury bazy danych do pliku SQL daje następujący wynik:

#
# Struktura tabeli `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;

#
# Struktura tabeli `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;

#
# Struktura tabeli `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. Entities JPA

W poniższej architekturze

Warstwa [Jpa] pełni rolę pomostu między obiektami obsługiwanymi przez warstwę [dao] a wierszami tabel bazy danych obsługiwanymi przez sterownik JDBC. Wiersze tabel odczytane z bazy danych są przekształcane w obiekty zwane encjami JPA. Z kolei podczas zapisu encje JPA są przekształcane w wiersze tabel. Encje te są przetwarzane przez wszystkie warstwy, a w szczególności przez warstwę internetową. Musimy więc je poznać:

Entyteta [Employe] reprezentuje wiersz tabeli [Employes]

ID
klucz podstawowy typu autoincrement
VERSION
numer wersji rekordu
PRENOM
imię pracownicy
NOM
jej nazwisko
ADRESSE
jej adres
CP
jego kod pocztowy
VILLE
jego miasto
INDEMNITE_ID
klucz zewnętrzny dla INDEMNITES (ID)

Entyteta [Employe] ma następujący wygląd:


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()
    +"]";
  }
  
  // metody pobierające i ustawiające
   ...  
}

Pominiemy adnotacje @ przeznaczone dla warstwy [Jpa]. Poszczególne pola klasy odzwierciedlają poszczególne kolumny tabeli [EMPLOYES]. Pole „indemnites” (wiersz 29) odzwierciedla fakt, że tabela [EMPLOYES] posiada klucz obcy do tabeli [INDEMNITES]. Podczas edycji danych pracownika edytowane są również jego dodatki.

Entyteta [Indemnite] jest wyrażeniem będącym obiektem wiersza tabeli [INDEMNITES]:

ID
klucz podstawowy typu autoincrement
VERSION
numer wersji rekordu
BASE_HEURE
koszt jednej godziny dyżuru w euro
ENTRETIEN_JOUR
dodatek w euro za dzień dyżuru
REPAS_JOUR
dodatek na posiłki w euro za dzień dyżuru
INDEMNITES_CP
dodatki z tytułu płatnego urlopu. Jest to procent
, który należy zastosować do wynagrodzenia podstawowego.

Entity [Indemnite] ma następującą postać:


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()
    + "]";
  }
  
   // metody pobierające i ustawiające
....  
}

Poszczególne pola klasy odzwierciedlają poszczególne kolumny tabeli [INDEMNITES].

Entyteta [Cotisation] jest wyrażeniem będącym obiektem wiersza tabeli [COTISATIONS]:

ID
klucz podstawowy typu autoincrement
VERSION
numer wersji rekordu
SECU
stawka (procentowa) składki na ubezpieczenie
społecznego
RETRAITE
stawka składki emerytalnej
CSGD
stawka składki na ubezpieczenie społeczne
podlegające odliczeniu
CSGRDS
stawka składki na ubezpieczenie społeczne
ogólnej oraz składki na spłatę
zadłużenia socjalnego

Jednostka [Cotisation] ma następujący opis:


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()+"]";
  }
  
  // metody pobierające i ustawiające
  ... 
}

Poszczególne pola klasy odzwierciedlają poszczególne kolumny tabeli [INDEMNITES].

18.4. Sposób obliczania wynagrodzenia opiekunki do dzieci

Aplikacja internetowa, którą zamierzamy stworzyć, pozwoli nam obliczyć wynagrodzenie pracownicy na podstawie trzech informacji:

  • numer identyfikacyjny pracownicy
  • liczba przepracowanych dni
  • liczba przepracowanych godzin

Oto zrzut ekranu przedstawiający obliczenie wynagrodzenia:

Image

Poniżej przedstawiamy sposób obliczania miesięcznego wynagrodzenia opiekunki do dzieci. Nie jest to sposób stosowany w rzeczywistości. Jako przykład posłużymy się wynagrodzeniem pani Marie Jouveinal, która w rozliczanym miesiącu przepracowała 150 godzin w ciągu 20 dni.

Uwzględniono następujące elementy:
[TOTALHEURES]: total des heures
 travaillées dans le mois
[TOTALJOURS]: total des jours
 travaillés dans le mois
[TOTALHEURES]=150
[TOTALJOURS]= 20
Wynagrodzenie podstawowe asystentki
opiekunki nad dziećmi oblicza się według
:
[SALAIREBASE]=([TOTALHEURES]
*[BASEHEURE])*(1+[INDEMNITESCP]/
100)
[SALAIREBASE]=(150*[2.1])
*(1+0.15)= 362,25
Pewna kwota składek na ubezpieczenie społeczne
należy potrącić z tego wynagrodzenia
:
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
Łączna kwota składek na ubezpieczenie społeczne:
[COTISATIONSSOCIALES]=[SALAIREBASE]
*(CSGRDS+CSGD+SECU+RETRAITE)/100
[COTISATIONSSOCIALES]=97,48
Ponadto opiekunka ma prawo,
za każdy przepracowany dzień do dodatku
na utrzymanie, a także do dodatku
posiłku. W związku z tym otrzymuje ona
następujące:
[Indemnités]=[TOTALJOURS]
*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104
Ostatecznie wynagrodzenie netto przysługujące opiekunce wynosi:
[SALAIREBASE]-
[COTISATIONSSOCIALES]+[INDEMNITÉS]
[salaire NET]=368,77

18.5. Interfejs warstwy [métier]

Wróćmy do architektury tworzonej przez nas aplikacji:

Warstwa [web / struts 2] komunikuje się z interfejsem warstwy [métier]. Wygląda on następująco:


package metier;

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

public interface IMetier {
  // pobieranie listy płac
  FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
  // lista pracowników
  List<Employe> findAllEmployes();
}
  • wiersz 8: metoda, która pozwoli nam obliczyć wynagrodzenie pracownika
  • wiersz 10: metoda, która pozwoli nam wypełnić listę rozwijaną pracowników

Metoda calculerFeuillesalaire zwraca instancję klasy [FeuilleSalaire] o następującej treści:


package metier;

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

public class FeuilleSalaire implements Serializable {
  // pola prywatne

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

  // konstruktory
  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 + "]";
  }

  // metody pobierające i ustawiające
  ...
}

Arkusz płacowy zawiera następujące informacje:

  • wiersz 10: informacje o pracowniku, dla którego obliczamy wynagrodzenie
  • wiersz 11: różne stawki składek
  • wiersz 12: składniki wynagrodzenia

Klasa [ElementsSalaire] ma następujący wygląd:


package metier;

import java.io.Serializable;

public class ElementsSalaire implements Serializable{
  
  // pola prywatne
  private double salaireBase;
  private double cotisationsSociales;
  private double indemnitesEntretien;
  private double indemnitesRepas;
  private double salaireNet;
  
  // konstruktory
  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 + "]";
  }
  
  // metody pobierające i ustawiające
  ...
}
  • wiersze 8–12: składniki wynagrodzenia

18.6. Plik konfiguracyjny Spring

Integrację warstw [métier, dao, jpa] zapewnia następujący plik konfiguracyjny 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">
  
  <!-- warstwy aplikacyjne -->
 
<!-- biznes -->
  <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" />
  
  <!-- konfiguracja 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>
  
  <!-- źródło danych 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>
  
  <!-- menedżer transakcji -->
  <tx:annotation-driven transaction-manager="txManager" />
  <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
  </bean>
  
  <!-- przetwarzanie wyjątków -->
  <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
  
  <!-- trwałość -->
  <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
  
</beans>

Nie będziemy próbować wyjaśniać tej konfiguracji. Jest ona niezbędna do instancjonowania i integracji warstw [métier, dao, jpa]. Nasza aplikacja internetowa, która będzie opierać się na tych warstwach, będzie zatem musiała przejąć tę konfigurację. Należy zauważyć, że wiersze 32–37 konfigurują parametry JDBC bazy danych. Czytelnik, który chciałby zmienić bazę danych, musi zmodyfikować te wiersze.

Więcej informacji na temat tej konfiguracji można znaleźć w dokumencie „Wprowadzenie do Java EE 5”, dostępnym na stronie URL [http://tahe.developpez.com/java/javaee].