18. دراسة حالة: Struts 2 / Tiles / Spring / Hibernate / MySQL
سنختتم مقدمتنا عن Struts بدراسة حالة. ولنكون واقعيين، فإن المثال الذي سندرسه سيكون أكثر تعقيدًا بكثير من تلك التي تمت تغطيتها سابقًا. بالنسبة للمبتدئين، من الأفضل ترسيخ فهمكم لأساسيات Struts 2 من خلال مشاريع شخصية قبل الشروع في دراسة الحالة هذه.
سيستخدم التطبيق بنية متعددة الطبقات:
![]() |
سيتم توفير طبقات [business] و [DAO] و [JPA/Hibernate] لنا في شكل أرشيف JAR، وسنقوم بتفصيل ميزاته. سيتولى Spring عملية تكامل هذه الطبقات. سيتم تنفيذ طبقة [web] باستخدام Struts 2.
![]() |
18.1. المشكلة
نقترح كتابة تطبيق ويب لإنشاء كشوف رواتب لمقدمي خدمات رعاية الأطفال العاملين في "مركز الطفولة المبكرة" التابع للبلدية.

يتم عرض دراسة الحالة هذه في الوثيقة:
مقدمة إلى Java EE 5 المتاح على الرابط [http://tahe.developpez.com/java/javaee]
في هذا المستند، تم تنفيذ دراسة الحالة باستخدام بنية متعددة المستويات التالية:
![]() |
يتم تنفيذ طبقة [الويب] باستخدام إطار عمل JSF (Java Server Faces). سنعتمد هذه البنية نفسها من خلال تنفيذ طبقة [الويب] باستخدام Struts 2. لإظهار مزايا البنى الطبقية، سنستخدم أرشيف JAR الذي يحتوي على طبقات [الأعمال، DAO، JPA] من إصدار JSF وربطه بطبقة [الويب / Struts 2]:
![]() |
سنقدم المكونات التالية لطبقات [الأعمال، DAO، JPA]:
- تتفاعل طبقة [الويب] مع طبقة [الأعمال]. سنقدم هذه الواجهة.
- تصل طبقة [JPA] إلى قاعدة البيانات. سنقدمها.
- تقوم طبقة [JPA] بتحويل الصفوف من جداول قاعدة البيانات إلى كيانات JPA التي تستخدمها جميع طبقات التطبيق. سنعرضها.
- يتم إنشاء مثيلات طبقات [الأعمال، DAO، JPA] بواسطة Spring. سنقدم ملف التكوين الذي يقوم بهذا الإنشاء والتكامل.
18.2. قاعدة البيانات
سنستخدم قاعدة بيانات MySQL التالية [dbpam_hibernate]:

- في [1]، تحتوي قاعدة البيانات على ثلاثة جداول:
- [employees]: جدول يسجل موظفي مركز رعاية الأطفال
- [contributions]: جدول يخزن معدلات اشتراكات الضمان الاجتماعي
- [indemnites]: جدول يخزن المعلومات المستخدمة لحساب رواتب الموظفين
جدول [employees]
![]() |
- في [2]، جدول الموظفين، وفي [3]، معنى حقوله
يمكن أن يكون محتوى الجدول كما يلي:
![]()
الجدول [contributions]
![]() |
- في [4]، جدول المساهمات، وفي [5]، معنى حقوله
يمكن أن يكون محتوى الجدول كما يلي:
![]()
الجدول [indemnites]
![]() |
- في [6]، جدول البدلات، وفي [7]، معنى حقوله
قد يكون محتوى الجدول كما يلي:
![]()
يؤدي تصدير بنية قاعدة البيانات إلى ملف SQL إلى النتيجة التالية:
18.3. كيانات JPA
في البنية التالية
![]() |
تعمل طبقة [Jpa] كجسر بين الكائنات التي تتعامل معها طبقة [dao] والصفوف في جداول قاعدة البيانات التي يتعامل معها برنامج تشغيل Jdbc. يتم تحويل الصفوف التي يتم قراءتها من جداول قاعدة البيانات إلى كائنات تسمى كيانات JPA. وبالمقابل، أثناء الكتابة، يتم تحويل كيانات JPA إلى صفوف في الجداول. يتم التعامل مع هذه الكيانات من قبل جميع الطبقات، ولا سيما طبقة الويب. لذلك، نحتاج إلى فهمها:
يمثل كيان [Employee] صفًا في جدول [Employees]
ID: المفتاح الأساسي من النوع autoincrement VERSION: رقم إصدار السجل FIRST_NAME: الاسم الأول للموظف LAST_NAME: لقب الموظفة ADDRESS: عنوان الموظفة ZIP: الرمز البريدي للموظف/الموظفة CITY: المدينة التي يقيم فيها الموظف/الموظفة INDEMNITY_ID: مفتاح خارجي مرتبط بـ INDEMNITIES(ID)

كيان [Employee] هو كما يلي:
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()
+"]";
}
// getters and setters
...
}
سنتجاهل التعليقات التوضيحية @ المخصصة لطبقة [Jpa]. تعكس الحقول المختلفة للفئة الأعمدة المختلفة لجدول [EMPLOYEES]. يعكس حقل التعويضات (السطر 29) حقيقة أن جدول [EMPLOYEES] يحتوي على مفتاح خارجي في جدول [INDEMNITIES]. عندما نقوم بمعالجة موظف، فإننا نقوم أيضًا بمعالجة تعويضاته.
الكيان [Indemnite] هو تمثيل الكائن لصف في جدول [INDEMNITIES]:
ID: مفتاح أساسي ذاتي التزايد VERSION: رقم إصدار السجل BASE_HEURE: التكلفة باليورو لساعة واحدة من الخدمة تحت الطلب DAILY_MAINTENANCE: البدل اليومي باليورو MEAL_DAY: بدل الوجبات باليورو لكل يوم رعاية PAID_LEAVE_ALLOWANCES: بدلات الإجازة المدفوعة. وهي نسبة مئوية تُطبق على الراتب الأساسي.

الكيان [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()
+ "]";
}
// getters and setters
....
}
تعكس الحقول المختلفة للفئة الأعمدة المختلفة لجدول [INDEMNITIES].
الكيان [Cotisation] هو تمثيل الكائن لصف في جدول [COTISATIONS]:
ID: المفتاح الأساسي من نوع التزايد التلقائي VERSION: رقم إصدار السجل SECU: معدل اشتراك الضمان الاجتماعي (نسبة مئوية) socialPENSION: معدل اشتراك المعاش التقاعدي CSGD: معدل الاشتراك في الضمان الاجتماعي العام Generalized Deductible: المبلغ المقتطع العام CSGRDS: معدل الاشتراك في سداد الديون الاجتماعية

الكيان [Contribution] هو كما يلي:
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()+"]";
}
// getters and setters
...
}
تعكس الحقول المختلفة للفئة الأعمدة المختلفة لجدول [INDEMNITES].
18.4. طريقة قائمة على [INDEMNITES] لحساب راتب مقدم رعاية الأطفال
سيسمح لنا التطبيق الويب الذي سنقوم بكتابته بحساب راتب الموظف بناءً على ثلاث معلومات:
- مؤشر الموظف
- عدد أيام العمل
- عدد ساعات العمل
فيما يلي لقطة شاشة لحساب الراتب:

سنعرض الآن طريقة حساب الراتب الشهري لمقدم رعاية الأطفال. لا يُقصد من هذه الطريقة أن تُستخدم في الواقع. كمثال، سنستخدم راتب السيدة ماري جوفينال، التي عملت 150 ساعة على مدار 20 يومًا خلال فترة الدفع.
يتم أخذ العوامل التالية في الاعتبار: | | |
الراتب الأساسي لـ يُحسب بالصيغة التالية : | | |
يجب خصم مبلغ معين من اشتراكات الضمان الاجتماعي يجب خصمه من هذا الراتب الأساسي : | | |
إجمالي اشتراكات الضمان الاجتماعي: | ||
بالإضافة إلى ذلك، يحق لمقدم رعاية الأطفال، عن كل يوم عمل، إلى بالإضافة إلى بدل وجبات . وبالتالي، فإنها تتلقى : | ||
وفي النهاية، يكون الراتب الصافي الذي سيُدفع لمقدمة رعاية الأطفال كما يلي: |
18.5. واجهة طبقة [الأعمال]
دعونا نلقي نظرة على بنية التطبيق الذي نقوم ببنائه:
![]() |
تتواصل طبقة [الويب / Struts 2] مع واجهة طبقة [الأعمال]. ويتم ذلك على النحو التالي:
package metier;
import java.util.List;
import jpa.Employe;
public interface IMetier {
// get your payslip
FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
// list of employees
List<Employe> findAllEmployes();
}
- السطر 8: الطريقة التي ستسمح لنا بحساب راتب الموظف
- السطر 10: الطريقة التي ستسمح لنا بتعبئة القائمة المنسدلة للموظفين
تُرجع الطريقة calculatePaystub مثيلًا لفئة [Paystub] التالية:
package metier;
import java.io.Serializable;
import jpa.Cotisation;
import jpa.Employe;
public class FeuilleSalaire implements Serializable {
// private fields
private Employe employe;
private Cotisation cotisation;
private ElementsSalaire elementsSalaire;
// manufacturers
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 and setters
...
}
تحتوي كعب الراتب على المعلومات التالية:
- السطر 10: معلومات عن الموظف الذي يتم حساب راتبه
- السطر 11: معدلات الاشتراكات المختلفة
- السطر 12: مكونات الراتب
فئة [ElementsSalaire] هي كما يلي:
package metier;
import java.io.Serializable;
public class ElementsSalaire implements Serializable{
// private fields
private double salaireBase;
private double cotisationsSociales;
private double indemnitesEntretien;
private double indemnitesRepas;
private double salaireNet;
// manufacturers
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 and setters
...
}
- الأسطر 8-12: مكونات الراتب
18.6. ملف تكوين Spring
يتم التعامل مع تكامل طبقات [الأعمال، 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">
<!-- application layers -->
<!-- business -->
<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" />
<!-- configuration 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>
<!-- the DBCP data source -->
<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>
<!-- transaction manager -->
<tx:annotation-driven transaction-manager="txManager" />
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<!-- translation of exceptions -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- persistence -->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
</beans>
لن نحاول شرح هذا التكوين. فهو ضروري لإنشاء مثيلات طبقات [الأعمال، DAO، JPA] وتكاملها. ولذلك، يجب أن يتبنى تطبيق الويب الخاص بنا، الذي سيعتمد على هذه الطبقات، هذا التكوين. لاحظ أن الأسطر 32– –37 تكوّن خصائص JDBC لقاعدة البيانات. يجب على القراء الذين يرغبون في تغيير قاعدة البيانات تعديل هذه الأسطر.
لمزيد من المعلومات حول هذا التكوين، راجع الوثيقة "مقدمة إلى Java EE 5" المتوفرة على الرابط [http://tahe.developpez.com/java/javaee].








