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

يتم عرض دراسة الحالة هذه في الوثيقة:

مقدمة إلى لغة جافا EE 5 المتاح على الرابط [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]، معاني حقوله

قد يكون محتوى الجدول كما يلي:

Image

الجدول [cotisations]

  • في [4]، جدول الاشتراكات، وفي [5]، معنى حقوله

قد يكون محتوى الجدول كما يلي:

Image

الجدول [indemnites]

  • في [6]، جدول التعويضات، وفي [7]، معنى حقولها

قد يكون محتوى الجدول كما يلي:

Image

يؤدي تصدير بنية قاعدة البيانات إلى ملف 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]

Image

ID: المفتاح الأساسي من النوع «التزايد التلقائي»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]:

Image

ID مفتاح أساسي من نوع التزايد التلقائيVERSIONnرقم إصدار السجل 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]:

Image

ID مفتاح أساسي من نوع التزايد التلقائيVERSION رقم إصدار السجلSECU نسبة (النسبة المئوية) لاشتراكات الضمان الاجتماعي RETRAITE معدل اشتراكات التقاعد CSGD معدل اشتراكات المساهمة الاجتماعية العامة القابلة للخصم CSGRD معدل اشتراكات المساهمة الاجتماعية العامة والمساهمة في سداد الدين الاجتماعي

الكيان [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 + "]";
  }

  // دالات الاسترجاع والتعيين
  ...
}

تحتوي ورقة الراتب على المعلومات التالية:

  • السطر 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 المتوفرة على الرابط [http://tahe.developpez.com/java/javaee].