18. دراسة حالة: Struts 2 / Tiles / Spring / Hibernate / MySQL
سنختتم تعلمنا لـ Struts بدراسة حالة. ولتحقيق الواقعية، سيكون المثال الذي سندرسه أكثر تعقيدًا بكثير من الأمثلة التي درسناها سابقًا. بالنسبة للمبتدئين، من الأفضل بلا شك التعمق في أساسيات Struts 2 من خلال تطبيقات شخصية قبل الشروع في دراسة هذه الحالة.
سيستخدم التطبيق بنية متعددة الطبقات:
![]() |
سيتم تزويدنا بمجموعة الطبقات [metier] و [dao] و [jpa/hibernate] في شكل ملف jar سنقوم بتفصيل وظائفه لاحقًا. وسيتم تكامل الطبقات بواسطة Spring. وسيتم تنفيذ الطبقة [web] بواسطة Struts 2.
![]() |
18.1. المشكلة
نقترح كتابة تطبيق ويب يسمح بإعداد كشوف رواتب المربيات اللواتي يعملن في «دار الطفولة المبكرة» التابعة لإحدى البلديات.

يتم عرض دراسة الحالة هذه في الوثيقة:
مقدمة إلى لغة جافا 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]:

- في [1]، تحتوي قاعدة البيانات على ثلاث جداول:
- [employes]: جدول يسجل الموظفات في حضانة
- [cotisations]: جدول يسجل معدلات الاشتراكات الاجتماعية
- [indemnites]: جدول يسجل المعلومات اللازمة لحساب رواتب الموظفات
الجدول [employes]
![]() |
- في [2]، جدول الموظفين، وفي [3]، معاني حقوله
قد يكون محتوى الجدول كما يلي:
![]()
الجدول [cotisations]
![]() |
- في [4]، جدول الاشتراكات، وفي [5]، معنى حقوله
قد يكون محتوى الجدول كما يلي:
![]()
الجدول [indemnites]
![]() |
- في [6]، جدول التعويضات، وفي [7]، معنى حقولها
قد يكون محتوى الجدول كما يلي:
![]()
يؤدي تصدير بنية قاعدة البيانات إلى ملف SQL إلى النتيجة التالية:
18.3. الكيانات JPA
في البنية التالية
![]() |
تقوم الطبقة [Jpa] بدور الجسر بين الكائنات التي تتعامل معها الطبقة [dao] وخطوط جداول قاعدة البيانات التي يتعامل معها برنامج التشغيل Jdbc. يتم تحويل خطوط الجداول التي يتم قراءتها من قاعدة البيانات إلى كائنات تسمى كيانات Jpa. وعلى العكس، عند الكتابة، تُحوَّل كيانات JPA إلى صفوف في الجداول. يتم التعامل مع هذه الكيانات من قِبل جميع الطبقات، ولا سيما طبقة الويب. لذا، يتعين علينا التعرف عليها:
يمثل الكيان [Employe] صفًّا في الجدول [Employes]

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]:

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]:

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. طريقة حساب راتب مربية الأطفال
سيسمح لنا تطبيق الويب الذي سنقوم بكتابته بحساب راتب الموظفة بناءً على ثلاث معلومات:
- رقم الموظفة
- عدد أيام العمل
- عدد ساعات العمل
فيما يلي لقطة شاشة لعملية حساب الراتب:

نقدم الآن طريقة حساب الراتب الشهري لمربية أطفال. ولا يُقصد بها أن تكون هي الطريقة المستخدمة في الواقع. نأخذ كمثال راتب السيدة ماري جوفينال التي عملت 150 ساعة على مدار 20 يومًا خلال الشهر محل الدفع.
يتم أخذ العناصر التالية في الاعتبار: | | |
يُحسب الراتب الأساسي لمساعدة يُحسب وفقًا للصيغة التالية: | | |
يجب اقتطاع عدد معين من الاشتراكات الاجتماعية يجب اقتطاعها من هذا الراتب الأساسي: | | |
إجمالي الاشتراكات الاجتماعية: | | |
بالإضافة إلى ذلك، يحق للمربية، عن كل يوم عمل، إلى بدل إعالة بالإضافة إلى بدل وجبة. وبهذا الصدد، تتلقى البدلات التالية: | | |
وفي النهاية، يكون الراتب الصافي المستحق للمربية كما يلي: | |
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 5» المتوفرة على الرابط [http://tahe.developpez.com/java/javaee].








