Skip to content

5. الإصدار 1: بنية Spring / JPA

نقترح كتابة تطبيق وحدة تحكم بالإضافة إلى تطبيق رسومي لإنشاء كشوف رواتب لمقدمي خدمات رعاية الأطفال العاملين في "Maison de la petite enfance" في إحدى البلديات. سيكون لهذا التطبيق البنية التالية:

5.1. قاعدة البيانات

سيتم تخزين البيانات الثابتة اللازمة لإنشاء كشف الراتب في قاعدة بيانات سنشير إليها باسم dbpam. قد تحتوي قاعدة البيانات هذه على الجداول التالية:

جدول الموظفين: يحتوي على معلومات حول مختلف مقدمي خدمات رعاية الأطفال

الهيكل:

المعرف
المفتاح الأساسي
الإصدار
رقم الإصدار – يزداد مع كل تعديل للصف
SS
رقم الضمان الاجتماعي للموظف – فريد
الاسم
اللقب
الاسم
الاسم الأول
العنوان
عنوانهم
المدينة
مدينته/مدينتها
الرمز البريدي
الرمز البريدي الخاص بهم
رقم التعويض
مفتاح خارجي في حقل [ID] في جدول [INDEMNITES]

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

Image

جدول COTISATIONS: يحتوي على النسب المئوية اللازمة لحساب اشتراكات الضمان الاجتماعي

الهيكل:

المعرف
المفتاح الأساسي
الإصدار
رقم الإصدار – يزداد مع كل تعديل للصف
CSGRDS
النسبة المئوية: المساهمة الاجتماعية العامة + المساهمة في سداد الديون الاجتماعية
CSGD
النسبة المئوية: الاشتراك الاجتماعي العام القابل للخصم
SECU
النسبة المئوية: الضمان الاجتماعي، الترمل، الشيخوخة
المعاش
النسبة المئوية: المعاش التكميلي + التأمين ضد البطالة

يمكن أن يكون محتواه كما يلي:

Image

معدلات اشتراكات الضمان الاجتماعي مستقلة عن الموظف. يحتوي الجدول السابق على صف واحد فقط.

جدول ALLOWANCES: يحتوي على العناصر المستخدمة لحساب الراتب المقرر دفعه.
رقم التعريف
المفتاح الأساسي
  
الإصدار
رقم الإصدار – يزداد مع كل تعديل للصف
  
الفهرس
فهرس المعالجة – فريد
  
السعر بالساعة
السعر الصافي باليورو لساعة واحدة من الخدمة تحت الطلب
  
الصيانة اليومية
البدل اليومي باليورو لكل يوم رعاية
  
وجبة في اليوم
بدل الوجبات باليورو لكل يوم رعاية
  
أجر الإجازة
بدل الإجازة المدفوعة. وهي نسبة مئوية تُطبق على الراتب الأساسي.
  
   

ويمكن أن يكون نصه كما يلي:

Image

يرجى ملاحظة أن البدلات قد تختلف من مقدم رعاية أطفال إلى آخر. فهي مرتبطة بمقدم رعاية أطفال معين من خلال رتبته الوظيفية. وبالتالي، فإن السيدة ماري جوفينال، التي تبلغ رتبتها الوظيفية 2 (جدول الموظفين)، تحصل على أجر ساعي قدره 2.1 يورو (جدول التعويضات).

5.2. طريقة حساب راتب مربية الأطفال

سنعرض الآن طريقة حساب الراتب الشهري لمربية الأطفال. ولا يُقصد من ذلك أن تكون هذه هي الطريقة المستخدمة فعليًا في الممارسة العملية. وكمثال على ذلك، سنستخدم راتب السيدة ماري جوفينال، التي عملت 150 ساعة على مدار 20 يومًا خلال فترة الدفع.

يتم أخذ العوامل التالية في الاعتبار:

[TOTALHOURS]: إجمالي الساعات
العمل خلال الشهر

[TOTALDAYS]: إجمالي أيام العمل
خلال الشهر
[TOTALHOURS]=150
[TOTALDAYS]= 20
الراتب الأساسي لمقدم رعاية الأطفال
يُحسب بالصيغة التالية:
[الراتب الأساسي]=([إجمالي الساعات]
*[HOURLYRATE])*(1+
[بدل_مقدم_الرعاية]/100)
[الراتب الأساسي]=
(150*[2.1])*(1+0.15)= 362.25
يجب خصم مبلغ معين من اشتراكات الضمان الاجتماعي
يجب خصمه من هذا الراتب الأساسي
:

الاشتراك الاجتماعي العام و
المساهمة في سداد
الدين الاجتماعي:
 [الراتب الأساسي]*[CSGRDS/100]

المساهمة الاجتماعية العامة القابلة للخصم:
 [الراتب الأساسي]*[CSGD/100]

الضمان الاجتماعي، ومستحقات الأرامل، ومستحقات الشيخوخة:
 [الراتب الأساسي]*[SECU/100]

المعاش التكميلي + صندوق التقاعد العام (AGPF) +
تأمين البطالة:
 [الراتب الأساسي]*[المعاش التقاعدي/100]
CSGRDS: 12.64
CSGD: 22.28
الضمان الاجتماعي: 34.02
المعاش التقاعدي: 28.55
إجمالي اشتراكات الضمان الاجتماعي:
[SOCIALCONTRIBUTIONS]=
[الراتب الأساسي]*(CSGRDS+CSGD
+SECU+PENSION)/100
[SOCIALCONTRIBUTIONS]=97.48
بالإضافة إلى ذلك، يحق لمقدمة رعاية الأطفال الحصول على بدل معيشة يومي وبدل وجبات عن كل يوم عمل. وبناءً على ذلك، تتلقى البدلات التالية:
[البدلات] = [إجمالي الأيام]
*(البدل اليومي + بدل الوجبات اليومي)
[البدلات]=104
في النهاية، يكون الراتب الصافي الذي سيُدفع لمقدم رعاية الأطفال كما يلي:
[الراتب الأساسي] - [اشتراكات الضمان الاجتماعي] + [البدلات]
[الراتب الصافي]=368.77

5.3. كيفية عمل تطبيق وحدة التحكم

فيما يلي مثال على تشغيل تطبيق وحدة التحكم في نافذة DOS:

dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar 254104940426058 150 20

Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé :
Nom : Jouveinal
Prénom : Marie
Adresse : 5 rue des Oiseaux
Ville : St Corentin
Code Postal : 49203
Indice : 2

Informations Cotisations :
CSGRDS : 3.49 %
CSGD : 6.15 %
Retraite : 7.88 %
Sécurité sociale : 9.39 %

Informations Indemnités :
Salaire horaire : 2.1 euro
Entretien/jour : 2.1 euro
Repas/jour : 3.1 euro
Congés Payés : 15.0 %

Informations Salaire :
Salaire de base : 362.25 euro
Cotisations sociales : 97.48 euro
Indemnités d'entretien : 42.0 euro
Indemnités de repas : 62.0 euro
Salaire net : 368.77 euro

سنكتب برنامجًا يتلقى المعلومات التالية:

  1. رقم الضمان الاجتماعي لمربية الأطفال (254104940426058 في المثال - السطر 1)
  2. إجمالي عدد ساعات العمل (150 في المثال - السطر 1)
  3. إجمالي عدد أيام العمل (20 في المثال - السطر 1)

نلاحظ ما يلي:

  • الأسطر 9-14: تعرض معلومات عن الموظف الذي تم تقديم رقم الضمان الاجتماعي الخاص به
  • الأسطر 17-20: تعرض معدلات الاشتراكات المختلفة
  • الأسطر 23-26: تعرض البدلات المرتبطة بدرجة راتب الموظف (هنا، الدرجة 2)
  • الأسطر 29–33: تعرض مكونات الراتب المقرر دفعه

يقوم التطبيق بتمييز أي أخطاء:

استدعاء بدون معلمات:


dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar
Syntaxe : pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés

استدعاء ببيانات غير صحيحة:


dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar  254104940426058 150x 20x
Le nombre d'heures travaillées [150x] est erroné
Le nombre de jours travaillés [20x] est erroné

اتصال برقم ضمان اجتماعي غير صحيح:


dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar  xx 150 20
L'erreur suivante s'est produite : L'employé de n°[xx] est introuvable

5.4. كيف يعمل التطبيق الرسومي

يحسب التطبيق الرسومي رواتب مقدمي خدمات رعاية الأطفال باستخدام نموذج Swing:

  • يتم الآن إدخال المعلومات التي تم تمريرها كمعلمات إلى برنامج وحدة التحكم باستخدام حقول الإدخال [1، 2، 3].
  • يبدأ الزر [4] عملية حساب الراتب
  • يعرض النموذج مكونات الراتب المختلفة حتى الراتب الصافي المقرر دفعه [5]

لا تعرض القائمة المنسدلة [1، 6] أرقام الضمان الاجتماعي للموظفين، بل أسمائهم الأولى والأخيرة. نفترض هنا أنه لا يوجد موظفان يحملان نفس الاسم الأول والأخير.

5.5. إنشاء قاعدة البيانات

نقوم بتشغيل WampServer ونستخدم أداة PhpMyAdmin [1]:

  • في [2]، حدد خيار [قواعد البيانات]،
  • في [3]، قم بإنشاء قاعدة بيانات [dbpam_hibernate
  • في [4]، تم إنشاء قاعدة البيانات. حددها،
  • في [5]، نريد استيراد نص SQL،
  • في [6]، استخدم زر [Browse] لتحديد الملف،
  • في [7،8]، حدد البرنامج النصي SQL،
  • في [9]، نقوم بتنفيذه،
  • في [10]، تم إنشاء الجداول. ومحتوياتها هي كما يلي:

جدول الموظفين

Image

جدول التعويضات

Image

جدول الاشتراكات

Image

5.6. تنفيذ JPA

5.6.1. طبقة JPA / Hibernate

سنقوم بتكوين طبقة JPA في البيئة التالية:

سيعمل برنامج وحدة التحكم مع قاعدة البيانات. للقيام بذلك، تحتاج إلى:

  • قاعدة بيانات،
  • برنامج تشغيل JDBC لنظام إدارة قواعد البيانات (DBMS)، وهو في هذه الحالة MySQL،
  • تنفيذ طبقة JPA باستخدام Hibernate،
  • كتابة برنامج وحدة التحكم.

نقوم بإنشاء مشروع Maven [mv-pam-jpa-hibernate] [1]:

في بنية تطبيقنا، نحتاج إلى العناصر التالية:

  • قاعدة البيانات،
  • برنامج تشغيل JDBC لنظام إدارة قواعد البيانات MySQL،
  • طبقة JPA/Hibernate (الكيانات والتكوين)،
  • برنامج وحدة التحكم في الاختبار.

5.6.1.1. قاعدة البيانات

أولاً، لنقم بإنشاء قاعدة بيانات فارغة. نقوم بتشغيل WampServer واستخدام أداة PhpMyAdmin [1]:

  • في [2]، حدد خيار [Databases
  • في [3]، قم بإنشاء قاعدة بيانات باسم [dbpam_hibernate
  • في [4]، تم إنشاء قاعدة البيانات.

5.6.1.2. تكوين طبقة JPA

يتم تكوين الاتصال بين طبقة JDBC وقاعدة البيانات في ملف [persistence.xml]، الذي يقوم بتكوين طبقة JPA. يمكن إنشاء هذا الملف باستخدام NetBeans:

  • في علامة التبويب [Services] [1]، قم بالاتصال بقاعدة البيانات باستخدام برنامج تشغيل MySQL JDBC [2]،
  • في [3]، اسم قاعدة البيانات التي تريد الاتصال بها.
  • في [4]، عنوان URL لـ JDBC الخاص بقاعدة البيانات،
  • في [5]، قم بتسجيل الدخول كجذر بدون كلمة مرور،
  • في [6]، يمكنك اختبار الاتصال،
  • في [7]، تم الاتصال بنجاح.
  • يظهر الرابط في [8] و[9]،
  • في [10]، أضف عنصرًا جديدًا إلى المشروع،
  • في [11] حدد فئة [Persistence] وفي [12] عنصر [Persistence Unit
  • في [13]، قم بتسمية وحدة الاستمرارية هذه،
  • في [14]، نختار تطبيق Hibernate،
  • في [15]، حدد الاتصال الذي أنشأناه للتو بقاعدة بيانات MySQL،
  • في [16]، حدد أنه عند إنشاء مثيل لطبقة JPA، يجب أن تقوم بإنشاء الجداول المطابقة لكيانات JPA الخاصة بالمشروع.

في نهاية المعالج، يتم إنشاء ملف [persistence.xml]:

  • يظهر الملف في فرع جديد من المشروع، في مجلد [META-INF] [1]،
  • والذي يتوافق مع مجلد [src/main/resources] الخاص بالمشروع [2,3].

ومحتواه كما يلي:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
      <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
    </properties>
  </persistence-unit>
</persistence>
  • السطر 3: اسم وحدة الاستمرارية ونوع المعاملة. يشير RESOURCE_LOCAL إلى أن المشروع يدير المعاملات بنفسه. في هذه الحالة، سيكون برنامج وحدة التحكم مسؤولاً عن القيام بذلك،
  • السطر 4: تنفيذ JPA المستخدم هو Hibernate،
  • الأسطر 6-9: خصائص JDBC لاتصال قاعدة البيانات،
  • السطر 11: يطلب إنشاء الجداول المطابقة لكيانات JPA. في الواقع، يقوم NetBeans بإنشاء تكوين غير صحيح هنا. يجب أن يكون التكوين كما يلي:

      <property name="hibernate.hbm2ddl.auto" value="create"/>

باستخدام الخيار create، يقوم Hibernate، عند إنشاء مثيل لطبقة JPA، بحذف الجداول المطابقة لكيانات JPA ثم إنشاءها. يقوم الخيار create-drop بنفس الشيء، ولكنه يحذف جميع الجداول في نهاية دورة حياة طبقة JPA. هناك خيار آخر:


      <property name="hibernate.hbm2ddl.auto" value="update"/>

يقوم هذا الخيار بإنشاء الجداول إذا لم تكن موجودة، ولكنه لا يحذفها إذا كانت موجودة بالفعل.

سنضيف ثلاث خصائص أخرى إلى تكوين Hibernate:


      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>

توجه هذه الإعدادات Hibernate لعرض عبارات SQL التي يرسلها إلى قاعدة البيانات. وبالتالي، يكون الملف الكامل كما يلي:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
      <property name="hibernate.hbm2ddl.auto" value="create"/>
      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
      <property name="use_sql_comments" value="true"/>
    </properties>
  </persistence-unit>
</persistence>

5.6.1.3. التبعيات

لنعد إلى بنية المشروع:

قمنا بتكوين طبقة JPA عبر ملف [persistence.xml]. وكان التنفيذ المختار هو Hibernate. وقد أدى ذلك إلى إدخال تبعيات في المشروع:

  

تعود هذه التبعيات إلى تضمين Hibernate في المشروع. نحتاج إلى إضافة تبعية أخرى: برنامج تشغيل MySQL JDBC، الذي ينفذ طبقة JDBC في البنية. نقوم بتحديث ملف [pom.xml] على النحو التالي:


<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>    
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>4.1.2</version>
    </dependency>
    ...
    <dependency>
      <groupId>org.hibernate.common</groupId>
      <artifactId>hibernate-commons-annotations</artifactId>
      <version>4.0.1.Final</version>
    </dependency>
  </dependencies>

تضيف الأسطر 8–12 التبعية لمحرك MySQL JDBC.

5.6.1.4. كيانات JPA


السؤال: باتباع النهج الوارد في المثال في القسم 4.4، قم بإنشاء الكيانات [Cotisation، Indemnite، Employe].


ملاحظات:

  • ستكون الكيانات جزءًا من حزمة باسم [jpa]،
  • وسيكون لكل كيان رقم إصدار،
  • إذا تم ربط كيانين بعلاقة، فسيتم إنشاء العلاقة الأساسية @ManyToOne فقط. ولن يتم إنشاء العلاقة العكسية @OneToMany.

5.6.1.5. كود الفئة الرئيسية

نقوم بتضمين كيانات JPA التي تم تطويرها مسبقًا [1] في المشروع:

ثم نضيف [2]، الفئة [main.Main] التالية:


package main;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
 
public class Main {
 
  public static void main(String[] args) {
    // creating the Entity Manager is enough to build the JPA layer
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-pam-jpa-hibernatePU");
    EntityManager em=emf.createEntityManager();
    // resource release
    em.close();
    emf.close();
  }
}
  • السطر 10: نقوم بإنشاء EntityManagerFactory لوحدة الاستمرارية المسماة [mv-pam-jpa-hibernatePU]. يأتي هذا الاسم من ملف [persistence.xml]:

  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    ...
  </persistence-unit>
  • السطر 12: يتم إنشاء EntityManager. يؤدي هذا إلى إنشاء طبقة JPA. سيتم استخدام ملف [persistence.xml]، وبالتالي سيتم إنشاء جداول قاعدة البيانات.
  • السطران 14-15: يتم تحرير الموارد.

5.6.1.6. الاختبارات

لنعد إلى بنية مشروعنا:

تم تنفيذ جميع الطبقات. نقوم بتشغيل المشروع [2].

إخراج وحدة التحكم كما يلي:

------------------------------------------------------------------------
Building mv-pam-jpa-hibernate 1.0-SNAPSHOT
------------------------------------------------------------------------

[resources:resources]
[debug] execute contextualize
Using 'UTF-8' encoding to copy filtered resources.
Copying 1 resource

[compiler:compile]
Nothing to compile - all classes are up to date

[exec:exec]
juin 21, 2012 4:22:47 PM org.hibernate.annotations.common.Version <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.1.Final}
juin 21, 2012 4:22:47 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.1.2}
juin 21, 2012 4:22:47 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
juin 21, 2012 4:22:47 PM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000402: Using Hibernate built-in connection pool (not for production use!)
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000115: Hibernate connection pool size: 20
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000006: Autocommit mode: true
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000401: using driver [com.mysql.jdbc.Driver] at URL [jdbc:mysql://localhost:3306/dbpam_hibernate]
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000046: Connection properties: {user=root, autocommit=true, release_mode=auto}
juin 21, 2012 4:22:48 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
juin 21, 2012 4:22:48 PM org.hibernate.engine.jdbc.internal.LobCreatorBuilder useContextualLobCreation
INFO: HHH000423: Disabling contextual LOB creation as JDBC driver reported JDBC version [3] less than 4
juin 21, 2012 4:22:48 PM org.hibernate.engine.transaction.internal.TransactionFactoryInitiator initiateService
INFO: HHH000268: Transaction strategy: org.hibernate.engine.transaction.internal.jdbc.JdbcTransactionFactory
juin 21, 2012 4:22:48 PM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport execute
INFO: HHH000227: Running hbm2ddl schema export
Hibernate: 
    alter table EMPLOYES 
        drop 
        foreign key FK75C8D6BC73F24A67
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport perform
ERROR: HHH000389: Unsuccessful: alter table EMPLOYES drop foreign key FK75C8D6BC73F24A67
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport perform
ERROR: Table 'dbpam_hibernate.employes' doesn't exist
Hibernate: 
    drop table if exists COTISATIONS
Hibernate: 
    drop table if exists EMPLOYES
Hibernate: 
    drop table if exists INDEMNITES
Hibernate: 
    create table COTISATIONS (
        id bigint not null auto_increment,
        CSGD double precision not null,
        CSGRDS double precision not null,
        RETRAITE double precision not null,
        SECU double precision not null,
        VERSION integer not null,
        primary key (id)
    )
Hibernate: 
    create table EMPLOYES (
        id bigint not null auto_increment,
        SS varchar(15) not null unique,
        ADRESSE varchar(50) not null,
        CP varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(20) not null,
        VERSION integer not null,
        VILLE varchar(30) not null,
        INDEMNITE_ID bigint not null,
        primary key (id)
    )
Hibernate: 
    create table INDEMNITES (
        id bigint not null auto_increment,
        BASE_HEURE double precision not null,
        ENTRETIEN_JOUR double precision not null,
        INDEMNITES_CP double precision not null,
        INDICE integer not null unique,
        REPAS_JOUR double precision not null,
        VERSION integer not null,
        primary key (id)
    )
Hibernate: 
    alter table EMPLOYES 
        add index FK75C8D6BC73F24A67 (INDEMNITE_ID), 
        add constraint FK75C8D6BC73F24A67 
        foreign key (INDEMNITE_ID) 
        references INDEMNITES (id)
juin 21, 2012 4:22:49 PM org.hibernate.tool.hbm2ddl.SchemaExport execute
INFO: HHH000230: Schema export complete
juin 21, 2012 4:22:49 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH000030: Cleaning up connection pool [jdbc:mysql://localhost:3306/dbpam_hibernate]
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 2.637s
Finished at: Thu Jun 21 16:22:49 CEST 2012
Final Memory: 8M/153M

تحتوي وحدة التحكم على سجلات Hibernate فقط، لأن البرنامج الذي تم تنفيذه لا يقوم بأي شيء سوى إنشاء مثيل لطبقة JPA. لاحظ النقاط التالية:

  • السطر 43: يحاول Hibernate حذف المفتاح الخارجي من جدول [EMPLOYEES
  • الأسطر 51–55: حذف الجداول الثلاثة،
  • السطر 57: إنشاء جدول [COTISATIONS
  • السطر 67: إنشاء جدول [EMPLOYEES
  • السطر 80: إنشاء جدول [INDEMNITIES
  • السطر 91: إنشاء المفتاح الخارجي لجدول [EMPLOYEES].

في NetBeans، يمكنك عرض الجداول في الاتصال الذي تم إنشاؤه مسبقًا:

تعتمد الجداول التي تم إنشاؤها على كل من تطبيق طبقة JPA المستخدم ونظام إدارة قواعد البيانات (DBMS) المستخدم. وبالتالي، يمكن لتطبيق JPA/EclipseLink مع نفس قاعدة البيانات أن يولد جداول مختلفة. وهذا ما سننظر فيه الآن.

سنقوم بإنشاء مشروع Maven جديد في البيئة التالية:

سنتبع الخطوات الواردة في القسم السابق:

  1. إنشاء قاعدة بيانات MySQL [dbpam_eclipselink]. سنستخدم البرنامج النصي [dbpam_eclipselink.sql] لإنشائها،
  2. إنشاء ملف [persistence.xml] الخاص بالمشروع. استخدم تطبيق EclipseLink JPA 2.0،
  3. أضف تبعية برنامج تشغيل MySQL JDBC إلى التبعيات التي تم إنشاؤها،
  4. إضافة كيانات JPA وبرنامج وحدة التحكم،
  5. تشغيل الاختبارات.

سيكون ملف [persistence.xml] كما يلي:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="pam-jpa-eclipselinkPU" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <properties>
      <property name="eclipselink.target-database" value="MySQL"/>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_eclipselink"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • تم إنشاء الخصائص 9–13 بواسطة معالج NetBeans،
  • السطر 14: تسمح لنا هذه الخاصية بتعيين مستوى تسجيل EclipseLink. يسمح لنا المستوى FINE برؤية عبارات SQL التي سيقوم EclipseLink بتنفيذها على قاعدة البيانات،
  • السطر 15: عند إنشاء مثيل لطبقة JPA/EclipseLink، سيتم حذف جداول كيانات JPA ثم إنشاؤها.

إخراج وحدة التحكم كما يلي:

------------------------------------------------------------------------
Building mv-pam-jpa-eclipselink 1.0-SNAPSHOT
------------------------------------------------------------------------

[resources:resources]
[debug] execute contextualize
Using 'UTF-8' encoding to copy filtered resources.
Copying 1 resource

[compiler:compile]
Nothing to compile - all classes are up to date

[exec:exec]
[EL Config]: 2012-06-22 14:35:01.852--ServerSession(730572764)--Thread(Thread[main,5,main])--The access type for the persistent class [class jpa.Cotisation] is set to [FIELD].
[EL Config]: 2012-06-22 14:35:01.884--ServerSession(730572764)--Thread(Thread[main,5,main])--The access type for the persistent class [class jpa.Employe] is set to [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Thread(Thread[main,5,main])--The target entity (reference) class for the many to one mapping element [field indemnite] is being defaulted to: class jpa.Indemnite.
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Thread(Thread[main,5,main])--The access type for the persistent class [class jpa.Indemnite] is set to [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Thread(Thread[main,5,main])--The alias name for the entity class [class jpa.Cotisation] is being defaulted to: Cotisation.
[EL Config]: 2012-06-22 14:35:01.915--ServerSession(730572764)--Thread(Thread[main,5,main])--The column name for element [id] is being defaulted to: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The alias name for the entity class [class jpa.Employe] is being defaulted to: Employe.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The column name for element [id] is being defaulted to: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The alias name for the entity class [class jpa.Indemnite] is being defaulted to: Indemnite.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The column name for element [id] is being defaulted to: ID.
[EL Config]: 2012-06-22 14:35:01.962--ServerSession(730572764)--Thread(Thread[main,5,main])--The primary key column name for the mapping element [field indemnite] is being defaulted to: ID.
[EL Info]: 2012-06-22 14:35:02.558--ServerSession(730572764)--Thread(Thread[main,5,main])--EclipseLink, version: Eclipse Persistence Services - 2.3.0.v20110604-r9504
[EL Config]: 2012-06-22 14:35:02.568--ServerSession(730572764)--Connection(1543921451)--Thread(Thread[main,5,main])--connecting(DatabaseLogin(
    platform=>MySQLPlatform
    user name=> "root"
    datasource URL=> "jdbc:mysql://localhost:3306/dbpam_eclipselink"
))
[EL Config]: 2012-06-22 14:35:02.738--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--Connected: jdbc:mysql://localhost:3306/dbpam_eclipselink
    User: root@localhost
    Database: MySQL  Version: 5.5.20-log
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-5.1.6 ( Revision: ${svn.Revision} )
[EL Info]: 2012-06-22 14:35:02.798--ServerSession(730572764)--Thread(Thread[main,5,main])--file:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU login successful
[EL Fine]: 2012-06-22 14:35:02.818--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--ALTER TABLE EMPLOYES DROP FOREIGN KEY FK_EMPLOYES_INDEMNITE_ID
[EL Fine]: 2012-06-22 14:35:03.088--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--DROP TABLE COTISATIONS
[EL Fine]: 2012-06-22 14:35:03.118--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--CREATE TABLE COTISATIONS (ID BIGINT NOT NULL, CSGD DOUBLE NOT NULL, CSGRDS DOUBLE NOT NULL, RETRAITE DOUBLE NOT NULL, SECU DOUBLE NOT NULL, VERSION INTEGER NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.198--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--DROP TABLE EMPLOYES
[EL Fine]: 2012-06-22 14:35:03.238--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--CREATE TABLE EMPLOYES (ID BIGINT NOT NULL, SS VARCHAR(15) NOT NULL UNIQUE, ADRESSE VARCHAR(50) NOT NULL, CP VARCHAR(5) NOT NULL, NOM VARCHAR(30) NOT NULL, PRENOM VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(30) NOT NULL, INDEMNITE_ID BIGINT NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.318--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--DROP TABLE INDEMNITES
[EL Fine]: 2012-06-22 14:35:03.338--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--CREATE TABLE INDEMNITES (ID BIGINT NOT NULL, BASE_HEURE DOUBLE NOT NULL, ENTRETIEN_JOUR DOUBLE NOT NULL, INDEMNITES_CP DOUBLE NOT NULL, INDICE INTEGER NOT NULL UNIQUE, REPAS_JOUR DOUBLE NOT NULL, VERSION INTEGER NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.418--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--ALTER TABLE EMPLOYES ADD CONSTRAINT FK_EMPLOYES_INDEMNITE_ID FOREIGN KEY (INDEMNITE_ID) REFERENCES INDEMNITES (ID)
[EL Fine]: 2012-06-22 14:35:03.568--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
[EL Fine]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Thread(Thread[main,5,main])--SELECT 1
[EL Warning]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Thread(Thread[main,5,main])--Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.0.v20110604-r9504): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'sequence' already exists
Error Code: 1050
Call: CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
Query: DataModifyQuery(sql="CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))")
[EL Fine]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values (SEQ_GEN, 0)
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--disconnect
[EL Info]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Thread(Thread[main,5,main])--file:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU logout successful
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Connection(1543921451)--Thread(Thread[main,5,main])--disconnect
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 3.503s
Finished at: Fri Jun 22 14:35:03 CEST 2012
Final Memory: 8M/153M
  • الأسطر 26-30: الاتصال بقاعدة بيانات MySQL،
  • الأسطر 31-34: تأكيد نجاح الاتصال،
  • السطر 36: حذف المفتاح الخارجي من جدول [EMPLOYEES
  • السطر 37: حذف الجدول [COTISATIONS
  • السطر 38: إنشاء جدول [CONTRIBUTIONS]. تجدر الإشارة إلى أن معرف المفتاح الأساسي لا يحتوي على سمة auto_increment في MySQL. وهذا يعني أن MySQL لا يقوم بإنشاء قيم المفتاح الأساسي،
  • السطر 39: حذف جدول [EMPLOYEES
  • السطر 40: إنشاء جدول [EMPLOYEES]. لا يحتوي معرف المفتاح الأساسي الخاص به على سمة auto_increment في MySQL،
  • السطر 41: حذف جدول [INDEMNITIES
  • السطر 42: إنشاء جدول [INDEMNITIES]. لا يحتوي معرف المفتاح الأساسي الخاص به على سمة auto_increment في MySQL،
  • السطر 43: إنشاء مفتاح خارجي من جدول [EMPLOYEES] إلى جدول [BENEFITS
  • السطر 44: إنشاء جدول [SEQUENCE]. سيتم استخدامه لتوليد المفاتيح الأساسية للجداول الثلاثة السابقة،
  • السطر 47: تحدث استثناء لأن هذا الجدول موجود بالفعل،
  • الأسطر 51–53: تهيئة الجدول [SEQUENCE].

يمكن التحقق من وجود الجداول التي تم إنشاؤها في NetBeans [1]:

وبالتالي، استنادًا إلى نفس كيانات JPA، لا تُنشئ تطبيقات Hibernate و EclipseLink JPA نفس الجداول. في بقية هذا المستند، عندما يكون تطبيق JPA المستخدم هو:

  • Hibernate، سنستخدم قاعدة البيانات [dbpam_hibernate
  • EclipseLink، سنستخدم قاعدة البيانات [dbpam_eclipselink].

5.6.3. العمل المطلوب

باتباع نفس الإجراء السابق،

  1. قم بإنشاء واختبار مشروع [mv-pam-jpa-hibernate-oracle] باستخدام تطبيق Hibernate JPA ونظام إدارة قواعد البيانات Oracle،
  2. قم بإنشاء واختبار مشروع [mv-pam-jpa-hibernate-mssql] باستخدام تطبيق Hibernate JPA ونظام إدارة قواعد البيانات SQL Server،
  3. قم بإنشاء واختبار مشروع [mv-pam-jpa-eclipselink-oracle] باستخدام تطبيق EclipseLink JPA ونظام إدارة قواعد البيانات Oracle،
  4. إنشاء واختبار مشروع [mv-pam-jpa-eclipselink-mssql] باستخدام تطبيق EclipseLink JPA ونظام إدارة قواعد البيانات SQL Server،

5.6.4. Lazy أم Eager؟

لنعد إلى تعريف محتمل لكيان [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(fetch= FetchType.LAZY)
  @JoinColumn(name="INDEMNITE_ID",nullable=false)
  private Indemnite indemnite;
  ...
}

تحدد الأسطر 27–29 المفتاح الخارجي من جدول [EMPLOYEES] إلى جدول [INDEMNITIES]. تحدد سمة fetch في السطر 27 استراتيجية الاسترجاع لحقل التعويض في السطر 29. هناك وضعان:

  • FetchType.LAZY: عند الاستعلام عن موظف، لا يتم استرداد التعويض المقابل. سيتم استرداده عند الإشارة إلى حقل [Employee].indemnity لأول مرة.
  • FetchType.EAGER: عند البحث عن موظف، يتم استرداد البدل المقابل. هذا هو الوضع الافتراضي عندما لا يتم تحديد أي وضع.

لفهم فائدة خيار FetchType.LAZY، انظر المثال التالي. يتم عرض قائمة بالموظفين بدون تعويضاتهم على صفحة ويب تحتوي على رابط [Details]. يؤدي النقر فوق هذا الرابط إلى عرض تعويض الموظف المحدد. نلاحظ ما يلي:

  • لعرض الصفحة الأولى، لا نحتاج إلى بيانات الموظفين ومزاياهم. ولذلك، فإن وضع FetchType.LAZY هو الأنسب؛
  • لعرض الصفحة الثانية مع التفاصيل، يجب إجراء استعلام إضافي إلى قاعدة البيانات لاسترداد مزايا الموظف المحدد.

يمنع وضع FetchType.LAZY استرداد الكثير من البيانات التي لا يحتاجها التطبيق على الفور. لنلقِ نظرة على مثال.

تم نسخ مشروع [mv-pam-jpa-hibernate]:

  • في [1]، يتم نسخ المشروع،
  • في [2]، نحدد المجلد للنسخة، وفي [3] نحدد اسمها،
  • في [4]، يحمل المشروع الجديد نفس اسم المشروع القديم. نقوم بتغيير هذا:
  • في [1]، نغير اسم المشروع،
  • في [2]، نغير اسم المشروع و artifactId الخاص به،
  • في [3]، المشروع الجديد.

نقوم بتعديل برنامج [Main.java] على النحو التالي:


package main;
 
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import jpa.Employe;
 
public class Main {
 
  // the JPQL query below brings back an employee
  // the foreign key [Employe].indemnite is in FetchType.LAZY
  public static void main(String[] args) {
    // creating the Entity Manager is enough to build the JPA layer
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("pam-jpa-hibernatePU");
    // first attempt
    EntityManager em = emf.createEntityManager();
    Employe employe = (Employe) em.createQuery("select e from Employe e where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
    em.close();
    // we display the employee
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // second test
    em = emf.createEntityManager();
    employe = (Employe) em.createQuery("select e from Employe e left join fetch e.indemnite where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
    // free up resources
    em.close();
    // we display the employee
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // resource release
    emf.close();
  }
}
  • السطر 15: نقوم بإنشاء EntityManagerFactory لطبقة JPA،
  • السطر 17: نحصل على EntityManager، الذي يسمح لنا بالتفاعل مع طبقة JPA،
  • السطر 18: نسترد الموظف المسمى Jouveinal،
  • السطر 19: نغلق EntityManager. يؤدي هذا إلى إغلاق سياق الاستمرارية.
  • السطر 22: نعرض الموظف الذي تم استرداده.

فئة [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(fetch= FetchType.LAZY)
  @JoinColumn(name="INDEMNITE_ID",nullable=false)
  private Indemnite indemnite;
 
 
  /**
   * Returns a string representation of the object.  This implementation constructs
   * that representation based on the id fields.
   * @return a string representation of the object.
   */
  @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()
    +"]";
  }
  ...
}
  • السطر 27: يتم جلب حقل indemnite في الوضع LAZY،
  • السطر 47: يستخدم حقل indemnite. إذا تم استدعاء طريقة toString بينما لم يتم استرداد حقل indemnite بعد، فسيتم استرداده في تلك اللحظة. ما لم يتم إغلاق سياق الاستمرارية، كما في المثال.

لنعد إلى كود [Main]:

  • الأسطر 21–25: يجب أن نحصل على استثناء. وذلك لأن طريقة toString سيتم استدعاؤها. وستستخدم حقل indemnite. سيتم البحث عن هذا الحقل. وبما أن سياق الاستمرارية قد تم إغلاقه، فإن كيان [Employee] الذي تم استرداده لم يعد موجودًا، ومن ثم يحدث الاستثناء.
  • السطر 27: نقوم بإنشاء EntityManager جديد،
  • السطر 28: نسترد الموظف Jouveinal عن طريق طلب البدل المرتبط به صراحةً في استعلام JPQL. هذا الطلب الصريح ضروري لأن وضع الاسترداد لهذا البدل هو LAZY،
  • السطر 30: نغلق EntityManager،
  • الأسطر 32-36: نعرض الموظف مرة أخرى. لا ينبغي أن يكون هناك استثناء.

لتشغيل المشروع، تحتاج إلى قاعدة بيانات تحتوي على بيانات. ستقوم بإنشائها باتباع الخطوات الواردة في القسم 5.5. بالإضافة إلى ذلك، يجب تعديل ملف [persistence.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
    </properties>
  </persistence-unit>
</persistence>
  • لقد أزلنا الخيار الذي كان ينشئ الجداول. قاعدة البيانات موجودة بالفعل ومملوءة،
  • وقد أزلنا الخيارات التي كانت تجعل Hibernate يسجل عبارات SQL التي أرسلها إلى قاعدة البيانات.

يؤدي تشغيل المشروع إلى ظهور الرسالتين التاليتين في وحدة التحكم:

org.hibernate.LazyInitializationException: could not initialize proxy - no Session
jpa.Employe[id=31,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
  • السطر 1: الاستثناء الذي حدث عند محاولة استرداد التعويض المفقود أثناء إغلاق الجلسة. يمكننا أن نرى أن التعويض لم يتم استرداده بسبب وضع LAZY،
  • السطر 2: الموظف مع بدلته تم استرداده عبر استعلام تجاوز وضع LAZY.

5.6.5. المهام المطلوب إنجازها

باتباع إجراء مشابه للإجراء الموصوف للتو، قم بإنشاء مشروع [mv-pam-pa-eclipselink-lazy] يوضح سلوك EclipseLink مع وضع LAZY.

تم الحصول على النتائج التالية:

jpa.Employe[id=453,version=1,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
jpa.Employe[id=453,version=1,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]

في الوضع LAZY، أعاد كلا الاستعلامين الأجر مع الموظف. عند البحث عن هذه الحالة الشاذة عبر الإنترنت، نكتشف أن التعليق التوضيحي [FetchType.LAZY] (السطر 1):


  @ManyToOne(fetch= FetchType.LAZY)
  @JoinColumn(name="INDEMNITE_ID",nullable=false)
private Indemnite indemnite;

ليس شرطًا بل اقتراحًا. ولا يُلزم مُنفِّذ JPA باتباعه. وبالتالي، يمكننا أن نرى أن الكود يصبح أحيانًا معتمدًا على تنفيذ JPA المستخدم. ومن الممكن تكوين EclipseLink ليعمل بالشكل المتوقع في الوضع LAZY.

5.6.6. المضي قدمًا

تكون بنية التطبيق المراد بناؤه كما يلي:

في بقية هذا المستند، سنقوم بنسخ مشروع Maven [mv-pam-jpa-hibernate] إلى مشروع [mv-pam-spring-hibernate] [1، 2، 3]:

  • ثم سنقوم بتغيير اسم المشروع الجديد [4، 5، 6].

سنقوم بتغيير التبعيات الخاصة بالمشروع الجديد. سيصبح ملف [pom.xml] كما يلي:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>istia.st</groupId>
  <artifactId>mv-pam-spring-hibernate</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
 
  <name>mv-pam-spring-hibernate</name>
  <url>http://maven.apache.org</url>
  <repositories>
    <repository>
      <url>http://repo1.maven.org/maven2/</url>
      <id>swing-layout</id>
      <layout>default</layout>
      <name>Repository for library Library[swing-layout]</name>
    </repository>
  </repositories>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
 
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>commons-dbcp</groupId>
      <artifactId>commons-dbcp</artifactId>
      <version>1.2.2</version>
    </dependency>
    <dependency>
      <groupId>commons-pool</groupId>
      <artifactId>commons-pool</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>4.1.2</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
    <dependency>
      <groupId>org.swinglabs</groupId>
      <artifactId>swing-layout</artifactId>
      <version>1.0.3</version>
    </dependency>
  </dependencies>
</project>
  • الأسطر 25–31: التبعية لاختبارات JUnit،
  • الأسطر 32–41: التبعيات لمجمع اتصالات Apache DBCP،
  • الأسطر 42–65: التبعيات الخاصة بإطار عمل Spring،
  • الأسطر 67–71: التبعيات لتنفيذ JPA/Hibernate،
  • الأسطر 72–76: التبعية لمحرك MySQL JDBC،
  • الأسطر 77–81: التبعية لواجهة Swing. يتم إضافة هذا تلقائيًا بواسطة NetBeans عند إضافة واجهة Swing إلى المشروع.

بالإضافة إلى ذلك، سنقوم بإنشاء قاعدتي بيانات MySQL:

  • [dbpam_hibernate] من البرنامج النصي [dbpam_hibernate.sql
  • [dbpam_eclipselink] من البرنامج النصي [dbpam_eclipselink.sql

5.7. واجهة im s لطبقات [business] و [DAO]

لنعد إلى بنية التطبيق:

في البنية أعلاه، ما هي الواجهة التي يجب أن توفرها طبقة [DAO] لطبقة [الأعمال]، وما هي الواجهة التي يجب أن توفرها طبقة [الأعمال] لطبقة [واجهة المستخدم]؟ تتمثل الطريقة الأولى لتعريف واجهات الطبقات المختلفة في دراسة حالات الاستخدام المتنوعة للتطبيق. لدينا هنا حالتان، اعتمادًا على واجهة المستخدم المختارة: وحدة التحكم أو النموذج الرسومي.

دعونا ندرس كيفية استخدام تطبيق وحدة التحكم:

dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar 254104940426058 150 20

Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé :
Nom : Jouveinal
...

Informations Cotisations :
CSGRDS : 3.49 %
...

Informations Indemnités :
...

Informations Salaire :
Salaire de base : 362.25 euro
Cotisations sociales : 97.48 euro
Indemnités d'entretien : 42.0 euro
Indemnités de repas : 62.0 euro
Salaire net : 368.77 euro

يتلقى التطبيق ثلاث معلومات من المستخدم (انظر السطر 1 أعلاه)

  • رقم الضمان الاجتماعي لمقدم رعاية الأطفال
  • عدد ساعات العمل في الشهر
  • عدد أيام العمل في الشهر

بناءً على هذه المعلومات والبيانات الأخرى المخزنة في ملفات التكوين، يعرض التطبيق المعلومات التالية:

  • الأسطر 4–6: القيم التي تم إدخالها
  • الأسطر 8-10: المعلومات المتعلقة بالموظف الذي تم تقديم رقم الضمان الاجتماعي الخاص به
  • الأسطر 12-14: معدلات مختلف اشتراكات الضمان الاجتماعي
  • السطور 16-17: المخصصات المختلفة المدفوعة لمقدم رعاية الأطفال
  • الأسطر 19-24: البنود الواردة في كشف راتب مقدم رعاية الأطفال

يجب أن تقدم الطبقة [التجارية] قدرًا معينًا من المعلومات إلى الطبقة [UI]:

  1. المعلومات المتعلقة بمقدم رعاية الأطفال المحدد برقم الضمان الاجتماعي الخاص به. توجد هذه المعلومات في جدول [EMPLOYEES]. وهذا يسمح بعرض الأسطر 6-8.
  2. مبالغ معدلات الاشتراكات المختلفة في الضمان الاجتماعي التي سيتم خصمها من الراتب الإجمالي. توجد هذه المعلومات في جدول [COTISATIONS]. وهذا يسمح بعرض الأسطر 10-12.
  3. مبالغ البدلات المختلفة المتعلقة بوظيفة مقدم رعاية الأطفال. توجد هذه المعلومات في جدول [INDEMNITES]. وهذا يسمح بعرض الأسطر 14-15.
  4. مكونات الراتب المعروضة في الأسطر 18-22.

من هذا، يمكننا اتخاذ قرار بشأن التنفيذ الأولي لواجهة [IMetier] التي تقدمها طبقة [metier] إلى طبقة [ui]:

1
2
3
4
5
6
package metier;

public interface IMetier {
   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
}
  • السطر 1: يتم وضع عناصر طبقة [business] في حزمة [business]
  • السطر 5: تأخذ طريقة [calculatePaystub] كمعلمات المعلومات الثلاث التي تم الحصول عليها من طبقة [ui] وتُرجع كائنًا من النوع [Paystub] يحتوي على المعلومات التي ستعرضها طبقة [ui] على وحدة التحكم. يمكن أن تكون فئة [ Paystub] كما يلي:
package metier;

import jpa.Cotisation;
import jpa.Employe;
import jpa.Indemnite;

public class FeuilleSalaire {
   // private fields
  private Employe employe;
  private Cotisation cotisation;
  private ElementsSalaire elementsSalaire;

  ...
}
  • السطر 9: الموظف المشمول بكشوف الراتب - المعلومات رقم 1 المعروضة بواسطة طبقة [ui]
  • السطر 10: معدلات الاشتراكات المختلفة - المعلومات رقم 2 المعروضة بواسطة طبقة [ui]
  • السطر 11: البدلات المختلفة المرتبطة بمؤشر الموظف - المعلومات رقم 3 المعروضة بواسطة طبقة [ui]
  • السطر 12: مكونات راتبه - المعلومات رقم 4 المعروضة بواسطة طبقة [ui]

تظهر حالة استخدام ثانية لطبقة [business] مع الواجهة الرسومية:

كما هو موضح أعلاه، تعرض القائمة المنسدلة [1, 2] جميع الموظفين. يجب طلب هذه القائمة من طبقة [الأعمال]. ثم تتطور واجهة ace لهذه الطبقة على النحو التالي:

package metier;

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

public interface IMetier {
   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // list of employees
  public List<Employe> findAllEmployes();
}
  • السطر [10]: الطريقة التي ستسمح لطبقة [UI] بطلب قائمة بجميع الموظفين من طبقة [Business].

لا يمكن لطبقة [الأعمال] تهيئة حقول [الموظف، والمساهمة، والبدل] الخاصة بكائن [كشوف المرتبات] أعلاه إلا من خلال الاستعلام عن طبقة [DAO]، حيث إن هذه المعلومات مخزنة في جداول قاعدة البيانات. وينطبق الأمر نفسه على استرداد قائمة بجميع الموظفين. يمكننا إنشاء واجهة [DAO] واحدة لإدارة الوصول إلى الكيانات الثلاثة [الموظف، المساهمة، البدل]. ومع ذلك، فقد قررنا هنا إنشاء واجهة [DAO] واحدة لكل كيان.

ستكون واجهة [DAO] للوصول إلى كيانات [Contribution] في جدول [CONTRIBUTIONS] كما يلي:

package dao;

import java.util.List;
import jpa.Cotisation;

public interface ICotisationDao {
       // create a new contribution
  public Cotisation create(Cotisation cotisation);
       // modify an existing contribution
  public Cotisation edit(Cotisation cotisation);
       // delete an existing contribution
  public void destroy(Cotisation cotisation);
       // search for a specific contribution
  public Cotisation find(Long id);
       // get all objects Contribution
  public List<Cotisation> findAll();

}
  • السطر 6: تدير واجهة [ICotisationDao] الوصول إلى كيان [Cotisation] وبالتالي إلى جدول [COTISATIONS] في قاعدة البيانات. لا يحتاج تطبيقنا سوى إلى الطريقة [findAll] في السطر 16، والتي تسترد جميع محتويات جدول [COTISATIONS]. هنا، أردنا معالجة حالة أكثر عمومية حيث يتم تنفيذ جميع عمليات CRUD (إنشاء، قراءة، تحديث، حذف) على الكيان.
  • السطر 8: تقوم طريقة [create] بإنشاء كيان [Cotisation] جديد
  • السطر 10: تعمل طريقة [edit] على تعديل كيان [Cotisation] موجود
  • السطر 12: تقوم طريقة [destroy] بحذف كيان [Cotisation] موجود
  • السطر 14: تسترد طريقة [find] كيان [Cotisation] موجودًا باستخدام معرّفه
  • السطر 16: تعرض طريقة [findAll] قائمة بجميع كيانات [Membership] الموجودة

دعونا نلقي نظرة فاحصة على توقيع طريقة [create]:

      // créer une nouvelle cotisation
Cotisation create(Cotisation cotisation);

تحتوي طريقة create على معلمة cotisation من نوع Cotisation. يجب أن يتم حفظ معلمة cotisation، أي تخزينها في جدول [COTISATIONS]. قبل هذا الحفظ، تحتوي معلمة cotisation على معرف id بدون قيمة. بعد الحفظ، يحتوي حقل id على قيمة تمثل المفتاح الأساسي للسجل المضاف إلى جدول [COTISATIONS]. وبالتالي، فإن معلمة cotisation هي معلمة إدخال/إخراج لطريقة create. لا يبدو من الضروري أن تقوم طريقة create بإرجاع معلمة cotisation كنتيجة إضافية. نظرًا لأن الطريقة المستدعية تحتفظ بمرجع إلى كائن [Cotisation cotisation]، فإنه في حالة تعديله، تتمتع الطريقة المستدعية بإمكانية الوصول إلى الكائن المعدل لأنها تحتفظ بمرجع إليه. وبالتالي، يمكنها معرفة القيمة التي عينتها طريقة create لحقل id في كائن [Cotisation cotisation]. وبالتالي، يمكن تبسيط توقيع الطريقة إلى:

      // créer une nouvelle cotisation
void create(Cotisation cotisation);

عند كتابة واجهة، من المهم تذكر أنه يمكن استخدامها في سياقين مختلفين: محلي وعن بُعد . في السياق المحلي، يتم تنفيذ الطريقة المستدعية والطريقة المستدعاة في نفس JVM:

إذا استدعت طبقة [الأعمال] طريقة create في طبقة [DAO]، فإنها تمتلك بالفعل مرجعًا إلى المعلمة [Membership membership] التي تمررها إلى الطريقة.

في السياق البعيد، يتم تنفيذ الطريقة المستدعية والطريقة المستدعاة في JVMs مختلفة:

في المثال أعلاه، تعمل طبقة [business] في JVM 1 وطبقة [DAO] في JVM 2 على جهازين مختلفين. لا تتواصل الطبقتان مباشرة. بينهما توجد طبقة سنسميها طبقة الاتصال [1]. تتكون هذه الطبقة من طبقة إرسال [2] وطبقة استقبال [3]. لا يتعين على المطور عمومًا كتابة طبقات الاتصال هذه. يتم إنشاؤها تلقائيًا بواسطة أدوات برمجية. تُكتب طبقة [الأعمال] كما لو كانت تعمل في نفس JVM التي تعمل فيها طبقة [DAO]. لذلك، لا يلزم إجراء أي تغييرات في الكود.

آلية الاتصال بين طبقة [الأعمال] وطبقة [DAO] هي كما يلي:

  • تستدعي طبقة [الأعمال] طريقة create الخاصة بطبقة [DAO]، وتمرر لها المعلمة [Contribution contribution1]
  • يتم تمرير هذه المعلمة فعليًا إلى طبقة الإرسال [2]. ستقوم هذه الطبقة بإرسال قيمة المعلمة cotisation1 عبر الشبكة، وليس مرجعها. يعتمد الشكل الدقيق لهذه القيمة على بروتوكول الاتصال المستخدم.
  • تسترد طبقة الاستقبال [3] هذه القيمة وتستخدمها لإعادة بناء كائن [Cotisation cotisation2] يعكس المعلمة الأولية المرسلة من طبقة [business]. لدينا الآن كائنان متطابقان (من حيث المحتوى) في جهازي JVM مختلفين: cotisation1 و cotisation2.
  • ستقوم طبقة العرض بتمرير كائن `contribution2` إلى طريقة `create` في طبقة [DAO]، والتي ستحفظه في قاعدة البيانات. بعد هذه العملية، تم تهيئة حقل `id` في كائن `contribution2` بالمفتاح الأساسي للسجل المضاف إلى جدول [COTISATIONS]. ليس هذا هو الحال بالنسبة للكائن `contribution1`، الذي تشير إليه طبقة [business]. إذا أردنا أن تشير طبقة [business] إلى الكائن cotisation2، فيجب علينا تمريره إلى الطبقة. لذلك، نحتاج إلى تغيير توقيع طريقة create في طبقة [DAO]:
      // créer une nouvelle cotisation
Cotisation create(Cotisation cotisation);
  • مع هذا التوقيع الجديد، ستُرجع طريقة create الكائن الثابت contribution2. يتم إرجاع هذه النتيجة إلى الطبقة المستقبلة [3] التي استدعت طبقة [DAO]. ستُرجع طبقة [DAO] قيمة (وليس مرجع) contribution2 إلى الطبقة المرسلة [2].
  • ستسترد الطبقة المرسلة [2] هذه القيمة وتستخدمها لإعادة بناء كائن [Membership membership3] يعكس النتيجة التي أعادتها طريقة create في طبقة [DAO].
  • يتم إرجاع الكائن [Contribution contribution3] إلى الطريقة في طبقة [business] التي أدى استدعاءها لطريقة create في طبقة [DAO] إلى بدء هذه الآلية بأكملها. وبالتالي، يمكن لطبقة [business] تحديد قيمة المفتاح الأساسي المخصصة للكائن [Contribution contribution1] الذي طلبت استمراريته: هذه هي قيمة حقل id في contribution3.

البنية السابقة ليست الأكثر شيوعًا. في أغلب الأحيان، توجد طبقات [business] و[DAO] في نفس JVM:

في هذه البنية، يجب أن تكون طرق طبقة [الأعمال] هي التي تُرجع النتائج، وليس طرق طبقة [DAO]. ومع ذلك، فإن التوقيع التالي لطريقة create في طبقة [DAO]:

      // créer une nouvelle cotisation
Cotisation create(Cotisation cotisation);

يسمح لنا بتجنب وضع افتراضات حول البنية الفعلية الموجودة. استخدام التوقيعات التي ستعمل بغض النظر عن البنية المختارة —سواء كانت محلية أو بعيدة— يعني أنه إذا قامت طريقة مستدعاة بتعديل بعض معلماتها:

  • يجب أن تكون هذه أيضًا جزءًا من نتيجة الدالة التي تم استدعاؤها
  • يجب أن تستخدم الطريقة المستدعية نتيجة الطريقة المستدعاة وليس المراجع إلى المعلمات المعدلة التي مررتها إلى الطريقة المستدعاة.

وهذا يسمح لنا بالانتقال من بنية محلية إلى بنية بعيدة دون تعديل الكود. دعونا نعيد النظر في واجهة [ICotisationDao] في ضوء ذلك:

package dao;

import java.util.List;
import jpa.Cotisation;

public interface ICotisationDao {
       // create a new contribution
  public Cotisation create(Cotisation cotisation);
       // modify an existing contribution
  public Cotisation edit(Cotisation cotisation);
       // delete an existing contribution
  public void destroy(Cotisation cotisation);
       // search for a specific contribution
  public Cotisation find(Long id);
       // get all objects Contribution
  public List<Cotisation> findAll();

}
  • السطر 8: تمت معالجة حالة طريقة create
  • السطر 10: تستخدم طريقة edit المعلمة [Cotisation cotisation1] لتحديث السجل في جدول [COTISATIONS] الذي له نفس المفتاح الأساسي لكائن cotisation. وتُرجع كائن cotisation2، الذي يمثل السجل المعدل. لا يتم تعديل المعلمة contribution1 نفسها. يجب أن تُرجع الطريقة contribution2 كنتيجة، سواء في بنية بعيدة أو محلية.
  • السطر 12: تحذف طريقة destroy السجل من جدول [COTISATIONS] الذي له نفس المفتاح الأساسي لكائن contribution الذي تم تمريره كمعلمة. لا يتم تعديل كائن contribution. لذلك، لا يلزم إرجاعه.
  • السطر 14: لا يتم تعديل المعلمة id للطريقة find بواسطة الطريقة. ولا يلزم تضمينها في النتيجة.
  • السطر 16: لا تحتوي طريقة findAll على أي معلمات. لذلك، لا داعي لفحصها.

في النهاية، لا يلزم سوى تكييف توقيع طريقة create لتكون قابلة للاستخدام ضمن بنية بعيدة. ينطبق المنطق المذكور أعلاه على واجهات [DAO] الأخرى. لن نكرره هنا، بل سنستخدم بدلاً من ذلك توقيعات قابلة للاستخدام في كل من البنى البعيدة والمحلية.

ستكون واجهة [DAO] للوصول إلى كيانات [Indemnite] في جدول [INDEMNITES] كما يلي:

package dao;

import java.util.List;
import jpa.Indemnite;

public interface IIndemniteDao {
     // create an Indemnity entity
  public Indemnite create(Indemnite indemnite);
     // modify an Indemnite entity
  public Indemnite edit(Indemnite indemnite);
     // delete an Indemnity entity
  public void destroy(Indemnite indemnite);
     // search for an Indemnite entity via its identifier
  public Indemnite find(Long id);
     // get all Indemnite entities
  public List<Indemnite> findAll();

}
  • السطر 6: تدير واجهة [IIndemniteDao] الوصول إلى كيان [Indemnite] وبالتالي إلى جدول [INDEMNITES] في قاعدة البيانات. لا يحتاج تطبيقنا سوى إلى الطريقة [findAll] في السطر 16، والتي تسترد محتويات جدول [INDEMNITES] بالكامل. هنا، أردنا معالجة حالة أكثر عمومية حيث يتم تنفيذ جميع عمليات CRUD (إنشاء، قراءة، تحديث، حذف) على الكيان.
  • السطر 8: تقوم طريقة [create] بإنشاء كيان [Indemnite] جديد
  • السطر 10: تعمل طريقة [edit] على تعديل كيان [Indemnite] موجود
  • السطر 12: تحذف طريقة [destroy] كيان [Indemnite] موجود
  • السطر 14: تسترد طريقة [find] كيان [Indemnite] موجود باستخدام معرّفه
  • السطر 16: تعرض طريقة [findAll] قائمة بجميع كيانات [Indemnite] الموجودة

ستكون واجهة [DAO] للوصول إلى كيانات [Employe] في جدول [EMPLOYES] كما يلي:

package dao;

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

public interface IEmployeDao {
     // create a new Employ entity
  public Employe create(Employe employe);
     // modify an existing Employe entity
  public Employe edit(Employe employe);
     // delete an Employ entity
  public void destroy(Employe employe);
     // search for an Employe entity via its id identifier
  public Employe find(Long id);
     // search for an Employe entity via its SS number
  public Employe find(String SS);
     // get all Employe entities
  public List<Employe> findAll();
}
  • السطر 6: تدير واجهة [IEmployeDao] الوصول إلى كيان [Employee] وبالتالي إلى جدول [EMPLOYEES] في قاعدة البيانات. لا يحتاج تطبيقنا سوى إلى الأسلوب [findAll] في السطر 16، الذي يسترد جميع محتويات الجدول [EMPLOYEES]. هنا، أردنا معالجة حالة أكثر عمومية حيث يتم تنفيذ جميع عمليات CRUD (إنشاء، قراءة، تحديث، حذف) على الكيان.
  • السطر 8: تقوم طريقة [create] بإنشاء كيان [Employee] جديد
  • السطر 10: تعمل طريقة [edit] على تعديل كيان [Employee] موجود
  • السطر 12: تقوم طريقة [destroy] بحذف كيان [Employee] موجود
  • السطر 14: تسترد طريقة [find] كيان [Employee] موجودًا عبر معرّفه
  • السطر 16: تسترد طريقة [find(String SS)] كيان [Employee] موجود باستخدام رقم SS الخاص به. وقد رأينا أن هذه الطريقة كانت ضرورية لتطبيق وحدة التحكم.
  • السطر 18: تعرض طريقة [findAll] قائمة بجميع كيانات [Employee] الموجودة. وقد رأينا أن هذه الطريقة كانت ضرورية لتطبيق الرسوميات.

5.8. فئة [PamException]

ستعمل طبقة [DAO] مع واجهة برمجة تطبيقات JDBC في Java. ترمي واجهة برمجة التطبيقات هذه استثناءات [SQLException] خاضعة للرقابة، والتي لها عيبان:

  • إنها تزيد من حجم الكود، الذي يجب أن يتعامل مع هذه الاستثناءات باستخدام كتل try/catch.
  • يجب الإعلان عنها في توقيعات طرق واجهة [IDao] باستخدام "throws SQLException". وهذا يمنع تنفيذ هذه الواجهة بواسطة الفئات التي قد ترمي استثناءً خاضعًا للرقابة من نوع غير [SQLException].

لحل هذه المشكلة، ستقوم طبقة [DAO] فقط بـ"نشر" الاستثناءات غير المحددة من النوع [PamException].

  • تقوم طبقة [JDBC] بإلقاء استثناءات من النوع [SQLException]
  • تقوم طبقة [JPA] بإلقاء استثناءات خاصة بتنفيذ JPA المستخدم
  • تقوم طبقة [DAO] بإلقاء استثناءات غير ملتقطة من النوع [PamException]

وهذا له نتيجتان:

  • لن تكون طبقة [business] مطالبة بمعالجة الاستثناءات من طبقة [DAO] باستخدام كتل try/catch. يمكنها ببساطة السماح لها بالانتشار حتى طبقة [UI].
  • لا تحتاج أساليب واجهة [IDao] إلى تحديد طبيعة [PamException] في توقيعاتها، مما يترك المجال مفتوحًا لتنفيذ هذه الواجهة باستخدام فئات من شأنها إلقاء نوع آخر من الاستثناءات غير الملتقطة.

سيتم وضع فئة [PamException] في حزمة [exception] لمشروع NetBeans:

وفيما يلي شفرة البرمجة الخاصة بها:

package exception;

@SuppressWarnings("serial")
public class PamException extends RuntimeException {

   // error code
  private int code;

  public PamException(int code) {
    super();
    this.code = code;
  }

  public PamException(String message, int code) {
    super(message);
    this.code = code;
  }

  public PamException(Throwable cause, int code) {
    super(cause);
    this.code = code;
  }

  public PamException(String message, Throwable cause, int code) {
    super(message, cause);
    this.code = code;
  }

   // getter and setter

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }

}
  • السطر 4: [PamException] مشتق من [RuntimeException]. ولذلك فهو نوع من الاستثناءات التي لا يطلب منا المُجمِّع معالجتها باستخدام كتلة try/catch أو تضمينها في توقيعات الطرق. ولهذا السبب، لا يتم تضمين [PamException] في توقيعات طرق واجهة [IDao]. وهذا يسمح بتنفيذ الواجهة بواسطة فئة ترمي نوعًا مختلفًا من الاستثناءات، بشرط أن تكون مشتقة أيضًا من [RuntimeException].
  • للتمييز بين الأخطاء التي قد تحدث، نستخدم رمز الخطأ في السطر 7. المنشئات الثلاثة في الأسطر 14 و19 و24 هي منشئات الفئة الأصلية [RuntimeException]، التي أضفنا إليها معلمة: رمز الخطأ الذي نريد تعيينه للاستثناء.

سيكون سلوك التطبيق، من منظور الاستثناءات، كما يلي:

  • ستقوم طبقة [DAO] بتغليف أي استثناء يتم مواجهته داخل [PamException] وإعادة إلقائه إلى طبقة [business].
  • ستسمح طبقة [business] للاستثناءات التي ترميها طبقة [DAO] بالانتشار إلى الأعلى. وستقوم بتغليف أي استثناء يحدث في طبقة [business] في [PamException] وإعادة رميها إلى طبقة [UI].
  • تقوم طبقة [UI] باعتراض جميع الاستثناءات التي تنتقل من طبقتي [business] و[DAO]. وستقوم ببساطة بعرض الاستثناء على وحدة التحكم أو واجهة المستخدم الرسومية.

دعونا الآن نفحص تنفيذ طبقتي [DAO] و[business] بالترتيب.

5.9. طبقة [DAO] لتطبيق [PAM]

نحن نعمل ضمن البنية التالية:

5.9.1. التنفيذ

قراءة موصى بها: القسم 3.1.3 من [المرجع 1]


السؤال: باستخدام تكامل Spring/JPA، اكتب الفئات [CotisationDao، IndemniteDao، EmployeDao] لتنفيذ الواجهات [ICotisationDao، IIndemniteDao، IEmployeDao]. ستقوم كل طريقة في الفئة بالتقاط أي استثناء وتغليفه في [PamException] مع رمز خطأ خاص بالاستثناء الذي تم التقاطه.


ستكون فئات التنفيذ جزءًا من حزمة [dao]:

  

5.9.2. التكوين

قراءة موصى بها: القسم 3.1.5 من [المرجع 1]

يتم تكوين تكامل DAO/JPA بواسطة ملف Spring [spring-config-dao.xml] وملف JPA [persistence.xml]:


السؤال: اكتب محتويات هذين الملفين. سنفترض أن قاعدة البيانات المستخدمة هي قاعدة بيانات MySQL5 [dbpam_hibernate] التي تم إنشاؤها بواسطة البرنامج النصي SQL [dbpam_hibernate.sql]. سيحدد ملف Spring الفئات الثلاثة التالية: employeDao من النوع EmployeDao، و indemniteDao من النوع IndemniteDao، و cotisationDao من النوع CotisationDao. علاوة على ذلك، سيكون تنفيذ JPA المستخدم هو Hibernate.


5.9.3. الاختبارات

قراءة موصى بها: الأقسام 3.1.6 و 3.1.7 من [ref1]

الآن بعد أن تمت كتابة طبقة [DAO] وتهيئتها، يمكننا اختبارها. ستكون بنية الاختبار كما يلي:

5.9.4. InitDB

سنقوم بإنشاء برنامجين اختباريين لطبقة [DAO]. سيتم وضعهما في الحزمة [dao] [2] التابعة لفرع [Test Packages] [1] في مشروع NetBeans. لا يتم تضمين هذا الفرع في المشروع الذي يتم إنشاؤه بواسطة خيار [Build project]، مما يضمن عدم تضمين البرامج الاختبارية التي نضعها هناك في ملف .jar النهائي للمشروع.

تتمتع الفئات الموضوعة في فرع [Test Packages] بإمكانية الوصول إلى الفئات الموجودة في فرع [Source Packages] وكذلك إلى مكتبات فئات المشروع. إذا كانت الاختبارات تتطلب مكتبات بخلاف تلك الموجودة في المشروع، فيجب الإعلان عنها في فرع [Test Libraries] [2].

تستخدم فئات الاختبار أداة اختبار الوحدات JUnit:

  • لا يقوم [JUnitInitDB] بإجراء أي اختبارات. بل يقوم بتعبئة قاعدة البيانات ببعض السجلات ثم يعرضها على وحدة التحكم.
  • [JUnitDao] يقوم بإجراء سلسلة من الاختبارات والتحقق من نتائجها.

هيكل فئة [JUnitInitDB] هو كما يلي:

package dao;

...

public class JUnitInitDB {

  private IEmployeDao employeDao = null;
  private ICotisationDao cotisationDao = null;
  private IIndemniteDao indemniteDao = null;

  @BeforeClass
  public void init(){
     // application configuration
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-dao.xml");
     // layers DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
  }

  @Test
  public void initDB(){
     // fill the base
...
     // displays the contents of the database
...
  }

  @Before()
  public void clean(){
     // empty the base
...
  }
}
  • يتم تنفيذ طريقة [init] قبل بدء مجموعة الاختبارات (التعليق التوضيحي @BeforeClass). وهي تقوم بإنشاء مثيل لطبقة [DAO].
  • يتم تنفيذ الطريقة [clean] قبل كل اختبار (التعليق التوضيحي @Before). وهي تقوم بمسح قاعدة البيانات.
  • طريقة [initDB] هي اختبار (التعليق التوضيحي @Test). وهي الطريقة الوحيدة. يجب أن يحتوي الاختبار على عبارات تأكيد Assert.assertCondition. هنا، لن يكون هناك أي منها. وبالتالي، فإن الطريقة هي اختبار وهمي. والغرض منها هو ملء قاعدة البيانات ببضعة صفوف ثم عرض محتويات قاعدة البيانات على وحدة التحكم. تُستخدم هنا طريقتا create و findAll من طبقات [DAO].

السؤال: أكمل الكود الخاص بفئة [JUnitInitDB]. استخدم المثال الوارد في القسم 3.1.6 من [ref1] كدليل. سيُنتج الكود الناتج الموضح في القسم 5.1.


5.9.5. تنفيذ الاختبار

نحن الآن جاهزون لتشغيل [InitDB]. نصف الإجراء باستخدام نظام إدارة قواعد البيانات MySQL5:

  • تم إعداد الفئات [1] وملفات التكوين [2] وفئات الاختبار الخاصة بطبقة [DAO] [3]،
  • يتم إنشاء المشروع [4]
  • يتم تنفيذ فئة [JUnitInitDB] [5]. يتم تشغيل نظام إدارة قواعد البيانات MySQL5 مع قاعدة بيانات [dbpam_hibernate] موجودة،
  • تشير نافذة [نتائج الاختبار] [6] إلى نجاح الاختبارات. هذه الرسالة ليست مهمة هنا، حيث لا يحتوي برنامج [JUnitInitDB] على أي عبارات تأكيد من نوع Assert.assertCondition قد تتسبب في فشل الاختبار. ومع ذلك، فهي توضح عدم حدوث أي استثناءات أثناء تنفيذ الاختبار.

تحتوي نافذة [Output] على سجلات التنفيذ، بما في ذلك تلك الخاصة بـ Spring والاختبار نفسه. الإخراج الذي تم إنشاؤه بواسطة فئة [JUnitInitDB] هو كما يلي:

------------- Standard Output ---------------
Employés ----------------------
jpa.Employe[id=5,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
jpa.Employe[id=6,version=0,SS=260124402111742,nom=Laverti,prenom=Justine,adresse=La brûlerie,ville=St Marcel,code postal=49014,indice=1]
Indemnités ----------------------
jpa.Indemnite[id=5,version=0,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0]
jpa.Indemnite[id=6,version=0,indice=2,base heure=2.1,entretien jour2.1,repas jour=3.1,indemnités CP=15.0]
Cotisations ----------------------
jpa.Cotisation[id=3,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88]
------------- ---------------- ---------------

تم ملء الجداول [EMPLOYEES، ALLOWANCES، CONTRIBUTIONS]. يمكن التحقق من ذلك عن طريق توصيل NetBeans بقاعدة البيانات [dbpam_hibernate].

  • في [1]، في علامة التبويب [services]، يمكنك عرض البيانات من جدول [employees] الخاص بالاتصال [dbpam_hibernate] [2]،
  • في [3] النتيجة.

5.9.6. JUnitD ao

سنلقي الآن نظرة على فئة اختبار ثانية [JUnitDao]:

سيكون الهيكل الأساسي للفئة كما يلي:

package dao;

import exception.PamException;
...

public class JUnitDao {

// layers DAO
  static private IEmployeDao employeDao;
  static private IIndemniteDao indemniteDao;
  static private ICotisationDao cotisationDao;

  @BeforeClass
  public static void init() {
     // log
    log("init");
     // application configuration
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-dao.xml");
     // layers DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
  }

  @AfterClass
  public static void terminate() {
  }

  @Before()
  public void clean() {
...
  }

   // logs
  private static void log(String message) {
    System.out.println("----------- " + message);
  }

   // tests
  @Test
  public void test01() {
    log("test01");
     // list of contributions
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // we add a contribution
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // on demand
    cotisation = cotisationDao.find(cotisation.getId());
     // check
    Assert.assertNotNull(cotisation);
    Assert.assertEquals(3.49, cotisation.getCsgrds(), 1e-6);
    Assert.assertEquals(6.15, cotisation.getCsgd(), 1e-6);
    Assert.assertEquals(9.39, cotisation.getSecu(), 1e-6);
    Assert.assertEquals(7.88, cotisation.getRetraite(), 1e-6);
     // we modify it
    cotisation.setCsgrds(-1);
    cotisation.setCsgd(-1);
    cotisation.setRetraite(-1);
    cotisation.setSecu(-1);
    Cotisation cotisation2 = cotisationDao.edit(cotisation);
     // checks
    Assert.assertEquals(cotisation.getVersion() + 1, cotisation2.getVersion());
    Assert.assertEquals(-1, cotisation2.getCsgrds(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getCsgd(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getRetraite(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getSecu(), 1e-6);
     // the modified element is requested
    Cotisation cotisation3 = cotisationDao.find(cotisation2.getId());
     // checks
    Assert.assertEquals(cotisation3.getVersion(), cotisation2.getVersion());
    Assert.assertEquals(-1, cotisation3.getCsgrds(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getCsgd(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getRetraite(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getSecu(), 1e-6);
     // we delete the
    cotisationDao.destroy(cotisation3);
     // checks
    Cotisation cotisation4 = cotisationDao.find(cotisation3.getId());
    Assert.assertNull(cotisation4);
    cotisations = cotisationDao.findAll();
    Assert.assertEquals(nbCotisations, cotisations.size());
  }


  @Test
  public void test02(){
    log("test02");
     // we ask for the list of allowances
...
     // we add an Indemnite indemnite
..
     // fetch indemnity from base - recover indemnity1
..
     // we check that indemnity1 = indemnity
...
     // modify the indemnity obtained and persist the modification in BD. The result is indemnity2
 ...
     // check the indemnite2 version
    ...
     // search for indemnity2 in base - obtain indemnity3
    ...
     // check that compensation3 = compensation2
    ...
     // delete indemnite3 image in base
    ...
     // we'll search for indemnite3 in base
    ...
     // check that a null reference has been obtained
 ...
  }

  @Test
  public void test03(){
    log("test03");
     // we repeat a test analogous to the previous ones for Employe
 ...
  }

  @Test
  public void test04(){
    log("test04");
     // test method [IEmployeDao].find(String SS)
     // first with an existing employee
     // then with a non-existent employee
...
  }

  @Test
  public void test05(){
    log("test05");
     // we create two allowances with the same index
     // violates index uniqueness constraint
     // check for the occurrence of a PamException exception
     // and has the expected error no
...
  }

  @Test
  public void test06(){
    log("test06");
     // create two employees with the same SS number
     // violates the uniqueness constraint on n° SS
     // check for the occurrence of a PamException exception
     // and has the expected error no
...

  }

  @Test
  public void test07(){
    log("test07");
     // create two employees with the same SS number, the 1st with create, the 2nd with edit
     // violates the uniqueness constraint on n° SS
     // check for the occurrence of a PamException exception
     // and has the expected error no
...
  }

  @Test
  public void test08(){
    log("test08");
     // deleting a non-existent employee does not trigger an exception
     // it is added and then destroyed - it is checked
...
  }

  @Test
  public void test09(){
    log("test09");
     // modifying an employee without having the correct version should trigger an exception
     // we check it
...
  }

  @Test
  public void test10(){
    log("test10");
     // deleting an employee without having the correct version should trigger an exception
     // we check it
...

  }

   // getters and setters
  ...
}

في فئة الاختبار السابقة، يتم مسح قاعدة البيانات قبل كل اختبار.


السؤال: اكتب الطرق التالية:

1 - test02: استنادًا إلى test01

2 - test03: لدى الموظف حقل من النوع Indemnity. لذلك، قم بإنشاء كيان Indemnity وكيان Employee

3 - test04.


بالمضي بنفس الطريقة المتبعة في فئة الاختبار [JUnitInitDB]، نحصل على النتائج التالية:

  • في [1]، نقوم بتشغيل فئة الاختبار
  • في [2]، تظهر نتائج الاختبار في نافذة [نتائج الاختبار]

دعونا نُحدث خطأً لنرى كيف يتم الإبلاغ عنه في صفحة النتائج:

  @Test
  public void test01() {
    log("test01");
     // list of contributions
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // we add a contribution
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // on demand
    cotisation = cotisationDao.find(cotisation.getId());
     // check
    Assert.assertNotNull(cotisation);
    Assert.assertEquals(0, cotisation.getCsgrds(), 1e-6);
    Assert.assertEquals(6.15, cotisation.getCsgd(), 1e-6);
    Assert.assertEquals(9.39, cotisation.getSecu(), 1e-6);
    Assert.assertEquals(7.88, cotisation.getRetraite(), 1e-6);
     // we modify it
....
}

السطر 13: سيؤدي هذا الافتراض إلى حدوث خطأ، لأن قيمة Csgrds تساوي 3.49 (السطر 8). ويؤدي تشغيل فئة الاختبار إلى النتائج التالية:

  • تُظهر صفحة النتائج [1] الآن أن بعض الاختبارات قد فشلت.
  • في [2]، ملخص للاستثناء الذي تسبب في فشل الاختبار. يتضمن رقم السطر في كود Java الذي حدث فيه الاستثناء.

5.10. طبقة [الأعمال] لتطبيق [PAM]

الآن بعد أن تمت كتابة طبقة [DAO]، ننتقل إلى دراسة طبقة الأعمال [2]:

5.10.1. واجهة Java [IMetier]

وقد تم وصفها في القسم 5.7. ونذكرها أدناه:

package metier;

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

public interface IMetier {
   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // list of employees
  public List<Employe> findAllEmployes();
}

سيتم تنفيذ طبقة [الأعمال] في حزمة [الأعمال]:

 

ستتضمن حزمة [الأعمال]، بالإضافة إلى واجهة [IMetier] وتنفيذها [Metier]، فئتين أخريين: [Payroll] و[PayrollItems]. تم تقديم فئة [Payroll] بإيجاز في القسم 5.7. سنعود إليها الآن.

5.10.2. فئة [Payroll]

تُرجع طريقة [calculatePayStub] الخاصة بواجهة [IMetier] كائنًا من نوع [PayStub] يمثل العناصر المختلفة لكشوفة الراتب. وفيما يلي تعريفها:

package metier;

import jpa.Cotisation;
import jpa.Employe;
import jpa.Indemnite;

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
  public String toString() {
    return "[" + employe + "," + cotisation + "," + elementsSalaire + "]";
  }

   // accessors
...  
}
  • السطر 7: تنفذ الفئة واجهة Serializable لأن مثيلاتها قد يتم تبادلها عبر الشبكة.
  • السطر 9: الموظف المشمول في كشف الراتب
  • السطر 10: معدلات الاشتراكات المختلفة
  • السطر 11: المخصصات المختلفة المرتبطة بمؤشر الموظف
  • السطر 12: مكونات راتبهم
  • الأسطر 14-22: منشئا الفئة
  • الأسطر 25-27: طريقة [toString] التي تحدد كائن [PayStub] معين
  • السطر 29 وما بعده: أدوات الوصول العامة إلى الحقول الخاصة بالفئة

تحتوي فئة [ElementsSalaire] المشار إليها في السطر 11 من فئة [FeuilleSalaire] أعلاه على العناصر التي تشكل كشف الراتب. وتعريفها كما يلي:

package metier;

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);
  }

   // toString
  public String toString() {
    return "[salaire base=" + salaireBase + ",cotisations sociales=" + cotisationsSociales + ",indemnités d'entretien="
      + indemnitesEntretien + ",indemnités de repas=" + indemnitesRepas + ",salaire net="
      + salaireNet + "]";
  }

   // public accessors
...  
}
  • السطر 3: تنفذ الفئة واجهة Serializable لأنها مكون من فئة PayrollClass، التي يجب أن تكون قابلة للتسلسل.
  • السطر 6: الراتب الأساسي
  • السطر 7: اشتراكات الضمان الاجتماعي المدفوعة على هذا الراتب الأساسي
  • السطر 8: مدفوعات إعالة الأطفال اليومية
  • السطر 9: بدلات وجبات الأطفال اليومية
  • السطر 10: الراتب الصافي الذي سيُدفع لمقدم رعاية الأطفال
  • السطور 12-24: منشئات الفئات
  • الأسطر 27-31: طريقة [toString] التي تحدد كائن [ElementsSalaire] معين
  • السطر 34 وما بعده: أدوات الوصول العامة إلى الحقول الخاصة بالفئة

5.10.3. فئة التنفيذ [Metier] للطبقة [business]

يمكن أن تكون فئة التنفيذ [Metier] للطبقة [business] كما يلي:

package metier;

...

@Transactional
public class Metier implements IMetier {

   // reference on the [DAO] layer
  private ICotisationDao cotisationDao = null;
  private IEmployeDao employeDao=null;


   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
    double nbHeuresTravaillées, int nbJoursTravaillés) {
...
  }

   // list of employees
   public List<Employe> findAllEmployes() {
     ...
  }

   // getters and setters
...
 }
  • السطر 5: تضمن علامة Spring @Transactional أن كل طريقة في الفئة تعمل ضمن معاملة.
  • السطران 9-10: إشارات إلى طبقات [DAO] لكيانات [Cotisation، Employe، Indemnite]
  • الأسطر 14–17: طريقة [calculatePayroll]
  • الأسطر 20–22: طريقة [findAllEmployees]
  • السطر 24 وما بعده: أدوات الوصول العامة للحقول الخاصة بالفئة

السؤال: اكتب كود طريقة [findAllEmployees].



السؤال: اكتب كود الأسلوب [calculatePayroll].


لاحظ النقاط التالية:

  • تم شرح طريقة حساب الراتب في القسم 5.2.
  • إذا لم تتطابق المعلمة [SS] مع أي موظف (أرجعت طبقة [DAO] مؤشرًا فارغًا)، فستقوم الطريقة بإلقاء استثناء [PamException] مع رمز خطأ مناسب.

5.10.4. اختبار طبقة [business]

نقوم بإنشاء برنامجين للاختبار:

يتم إنشاء فئات الاختبار [3] في حزمة [metier] [2] ضمن فرع [Test Packages] [1] للمشروع.

قد تبدو فئة [JUnitMetier_1] كما يلي:

package metier;

...

public class JUnitMetier_1 {

// business layer
  private IMetier metier;

  @BeforeClass
  public void init(){
     // log
    log("init");
     // application configuration
     // instantiation layer [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
    metier = (IMetier) ctx.getBean("metier");
     // layers DAO
    IEmployeDao employeDao=(IEmployeDao) ctx.getBean("employeDao");
    ICotisationDao cotisationDao=(ICotisationDao) ctx.getBean("cotisationDao");
    IIndemniteDao indemniteDao=(IIndemniteDao) ctx.getBean("indemniteDao");
     // empty the base
    for(Employe employe:employeDao.findAll()){
      employeDao.destroy(employe);
    }
    for(Cotisation cotisation:cotisationDao.findAll()){
      cotisationDao.destroy(cotisation);
    }
    for(Indemnite indemnite : indemniteDao.findAll()){
      indemniteDao.destroy(indemnite);
    }
     // fill it
    Indemnite indemnite1=indemniteDao.create(new Indemnite(1,1.93,2,3,12));
    Indemnite indemnite2=indemniteDao.create(new Indemnite(2,2.1,2.1,3.1,15));
    Employe employe2=employeDao.create(new Employe("254104940426058","Jouveinal","Marie","5 rue des oiseaux","St Corentin","49203",indemnite2));
    Employe employe1=employeDao.create(new Employe("260124402111742","Laverti","Justine","La brûlerie","St Marcel","49014",indemnite1));
    Cotisation cotisation1=cotisationDao.create(new Cotisation(3.49,6.15,9.39,7.88));
  }

   // logs
  private void log(String message) {
    System.out.println("----------- " + message);
  }

   // test
  @Test
  public void test01(){
     // wage sheet calculation
    System.out.println(metier.calculerFeuilleSalaire("260124402111742",30, 5));
    System.out.println(metier.calculerFeuilleSalaire("254104940426058",150, 20));
    try {
      System.out.println(metier.calculerFeuilleSalaire("xx", 150, 20));
    } catch (PamException ex) {
      System.err.println(String.format("PamException[Code=%d, message=%s]",ex.getCode(), ex.getMessage()));
    }
  }
}

لا توجد أي تأكيدات Assert.assertCondition في الفئة. نحن نحاول ببساطة حساب بعض الرواتب حتى نتمكن بعد ذلك من التحقق منها يدويًا. فيما يلي إخراج الشاشة الناتج عن تشغيل الفئة السابقة:

1
2
3
4
5
6
7
Testsuite: metier.JUnitMetier_1
----------- init
....
[jpa.Employe[id=22,version=0,SS=260124402111742,nom=Laverti,prenom=Justine,adresse=La brûlerie,ville=St Marcel,code postal=49014,indice=1],jpa.Cotisation[id=6,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88],jpa.Indemnite[id=29,version=0,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0],[salaire base=64.85,cotisations sociales=17.45,indemnités d'entretien=10.0,indemnités de repas=15.0,salaire net=72.4]]
[jpa.Employe[id=21,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2],jpa.Cotisation[id=6,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88],jpa.Indemnite[id=30,version=0,indice=2,base heure=2.1,entretien jour2.1,repas jour=3.1,indemnités CP=15.0],[salaire base=362.25,cotisations sociales=97.48,indemnités d'entretien=42.0,indemnités de repas=62.0,salaire net=368.77]]
PamException[Code=101, message=L'employé de n°[xx] est introuvable]
Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 3,234 sec
  • السطر 4: كشف راتب جوستين لافيرتي
  • السطر 5: كشف راتب ماري جوفينال
  • السطر 6: الاستثناء الناتج عن عدم وجود موظف برقم الضمان الاجتماعي "xx".

السؤال: يستخدم السطر 17 من [JUnitMetier_1] حبة Spring المسماة metier. قدم تعريف هذه الحبة في الملف [spring-config-metier-dao.xml].


يمكن أن تكون الفئة [JUnitMetier_2] كما يلي:

package metier;

...
public class JUnitMetier_2 {

// business layer
  private IMetier metier;

  @BeforeClass
  public void init(){
...
  }

   // logs
  private void log(String message) {
    System.out.println("----------- " + message);
  }

   // test
  @Test
  public void test01(){
...
  }
}

تُعد فئة [JUnitMetier_2] نسخة من فئة [JUnitMetier_1]، باستثناء أن عمليات التحقق قد أُدرجت هذه المرة في الدالة test01.


السؤال: اكتب طريقة test01.


عند تنفيذ الفئة [JUnitMetier_2]، يتم الحصول على النتائج التالية إذا سارت الأمور على ما يرام:

Image

5.11. طبقة [ui] لتطبيق [PAM] – إصدار وحدة التحكم

الآن بعد أن تمت كتابة طبقة [business]، لا يزال يتعين علينا كتابة طبقة [ui] [1]:

سنقوم بإنشاء نسختين مختلفتين من طبقة [ui]: نسخة تعمل على وحدة التحكم ونسخة تعمل على واجهة المستخدم الرسومية Swing:

5.11.1. فئة [ ui.console.Main]

سنركز أولاً على تطبيق وحدة التحكم الذي تم تنفيذه بواسطة فئة [ui.console.Main] أعلاه. وقد تم وصف طريقة عملها في القسم 5.3. يمكن أن يكون الهيكل الأساسي لفئة [Main] كما يلي:

package ui.console;

import exception.PamException;
import metier.FeuilleSalaire;
import metier.IMetier;

import java.util.ArrayList;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

  /**
   * @param args
   */
  public static void main(String[] args) {
     // local data
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
     // check the number of parameters
...
     // error list
    ArrayList erreurs = new ArrayList();
     // the second parameter must be a real number >0
...
     // mistake?
    if (...) {
      erreurs.add("Le nombre d'heures travaillées [" + args[1]
        + "] est erroné");
    }
     // the third parameter must be an integer >0
...
     // mistake?
    if (...) {
      erreurs.add("Le nombre de jours travaillés [" + args[2]
        + "] est erroné");
    }
     // mistakes?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
     // it's OK - we can ask for the payslip
    FeuilleSalaire feuilleSalaire = null;
    try {
       // instantiation layer [metier]
      ...
       // wage sheet calculation
      ...
    } catch (PamException ex) {
      System.err.println("L'erreur suivante s'est produite : "+ ex.getMessage());
      return;
    } catch (Exception ex) {
      System.err.println("L'erreur suivante s'est produite : "+ ex.toString());
      return;
    }

     // detailed display
    String output = "Valeurs saisies :\n";
    output += ajouteInfo("N° de sécurité sociale de l'employé", args[0]);
    output += ajouteInfo("Nombre d'heures travaillées", args[1]);
    output += ajouteInfo("Nombre de jours travaillés", args[2]);
    output += ajouteInfo("\nInformations Employé", "");
    output += ajouteInfo("Nom", feuilleSalaire.getEmploye().getNom());
    output += ajouteInfo("Prénom", feuilleSalaire.getEmploye().getPrenom());
    output += ajouteInfo("Adresse", feuilleSalaire.getEmploye().getAdresse());
    output += ajouteInfo("Ville", feuilleSalaire.getEmploye().getVille());
    output += ajouteInfo("Code Postal", feuilleSalaire.getEmploye().getCodePostal());
    output += ajouteInfo("Indice", ""+ feuilleSalaire.getEmploye().getIndemnite().getIndice());
    output += ajouteInfo("\nInformations Cotisations", "");
    output += ajouteInfo("CSGRDS", ""+ feuilleSalaire.getCotisation().getCsgrds() + " %");
    output += ajouteInfo("CSGD", ""+ feuilleSalaire.getCotisation().getCsgd() + " %");
    output += ajouteInfo("Retraite", ""+ feuilleSalaire.getCotisation().getRetraite() + " %");
    output += ajouteInfo("Sécurité sociale", ""+ feuilleSalaire.getCotisation().getSecu() + " %");
    output += ajouteInfo("\nInformations Indemnités", "");
    output += ajouteInfo("Salaire horaire", ""+ feuilleSalaire.getEmploye().getIndemnite().getBaseHeure() + " euro");
    output += ajouteInfo("Entretien/jour", ""+ feuilleSalaire.getEmploye().getIndemnite().getEntretienJour() + " euro");
    output += ajouteInfo("Repas/jour", ""+ feuilleSalaire.getEmploye().getIndemnite().getRepasJour() + " euro");
    output += ajouteInfo("Congés Payés", ""+ feuilleSalaire.getEmploye().getIndemnite().getIndemnitesCP()+ " %");
    output += ajouteInfo("\nInformations Salaire", "");
    output += ajouteInfo("Salaire de base", ""+ feuilleSalaire.getElementsSalaire().getSalaireBase()+ " euro");
    output += ajouteInfo("Cotisations sociales", ""+ feuilleSalaire.getElementsSalaire().getCotisationsSociales()+ " euro");
    output += ajouteInfo("Indemnités d'entretien", ""+ feuilleSalaire.getElementsSalaire().getIndemnitesEntretien()+ " euro");
    output += ajouteInfo("Indemnités de repas", ""+ feuilleSalaire.getElementsSalaire().getIndemnitesRepas()+ " euro");
    output += ajouteInfo("Salaire net", ""+ feuilleSalaire.getElementsSalaire().getSalaireNet() + " euro");

    System.out.println(output);
  }

  static String ajouteInfo(String message, String valeur) {
    return message + " : " + valeur + "\n";
  }
}

السؤال: أكمل الكود أعلاه.


5.11.2. التنفيذ

لتشغيل فئة [ui.console.Main]، اتبع الخطوات التالية:

  • في [1]، حدد خصائص المشروع،
  • في [2]، حدد خاصية [Run] للمشروع،
  • استخدم الزر [3] لتحديد الفئة (المعروفة باسم الفئة الرئيسية) المراد تشغيلها،
  • حدد الفئة [4]،
  • تظهر الفئة في [5]. تتطلب هذه الفئة ثلاث معلمات للتشغيل (رقم الضمان الاجتماعي، عدد ساعات العمل، عدد أيام العمل). يتم إدخال هذه المعلمات في [6]،
  • وبمجرد الانتهاء من ذلك، يمكن تنفيذ المشروع [7]. يعني التكوين السابق أن فئة [ui.console.Main] سيتم تنفيذها.

يتم عرض نتائج التنفيذ في نافذة [output]:

5.12. طبقة [ui] لتطبيق [PAM] – النسخة الرسومية

سنقوم الآن بتنفيذ طبقة [ui] بواجهة مستخدم رسومية:

  • في [1]، فئة [PamJFrame] للواجهة الرسومية
  • في [2]: واجهة المستخدم الرسومية

5.12.1. دليل تعليمي سريع

لإنشاء واجهة المستخدم الرسومية، اتبع الخطوات التالية:

  • [1]: أنشئ ملفًا جديدًا باستخدام الزر [1] [ملف جديد...]
  • [2]: حدد فئة الملف [Swing GUI Forms]، أي النماذج الرسومية
  • [3]: حدد النوع [JFrame Form]، وهو نوع نموذج فارغ
  • [5]: قم بتسمية النموذج؛ وسيكون هذا أيضًا اسم الفئة
  • [6]: ضع النموذج في حزمة
  • [8]: تمت إضافة النموذج إلى شجرة المشروع
  • [9]: يمكن الوصول إلى النموذج عبر عرضين: [التصميم] [9]، الذي يسمح لك بتصميم المكونات المختلفة للنموذج، و[المصدر] [10 أدناه]، الذي يوفر الوصول إلى كود Java الخاص بالنموذج. في النهاية، النموذج هو فئة Java مثل أي فئة أخرى. طريقة العرض [التصميم] هي أداة لتصميم النموذج. في كل مرة تتم فيها إضافة مكون في وضع [التصميم]، يتم إضافة كود Java في طريقة العرض [المصدر] لتعويض ذلك.
  • [11]: يمكن العثور على قائمة مكونات Swing المتاحة للنموذج في نافذة [Palette].
  • [12]: تعرض نافذة [Inspector] الهيكل الشجري لمكونات النموذج. توجد المكونات ذات التمثيل المرئي في فرع [JFrame]؛ أما المكونات الأخرى فتوجد في فرع [Other Components].
  • في [13]، نختار مكون [JLabel] بنقرة واحدة
  • في [14]، نقوم بإفلاته على النموذج في وضع [Design]
  • في [15]، نحدد خصائص JLabel (النص، الخط).
  • في [16]، النتيجة.
  • في [17]، نطلب معاينة للنموذج
  • في [18]، النتيجة
  • في [19]، تمت إضافة التسمية [JLabel1] إلى شجرة المكونات في نافذة [Inspector]
  • في [20] و[21]: في عرض [Source] للنموذج، تمت إضافة كود Java للتعامل مع JLabel المضافة.

يتوفر دليل تعليمي حول إنشاء النماذج باستخدام NetBeans على الرابط [http://www.netbeans.org/kb/trails/matisse.html].

5.12.2. واجهة المستخدم الرسومية [PamJFrame]

سنقوم بإنشاء واجهة المستخدم الرسومية التالية:

  • في [1]، واجهة المستخدم الرسومية
  • في [2]، هيكل الشجرة لمكوناتها: عنصر JLabel وستة حاويات JPanel

JLabel1

JPanel1

JPanel2

JPanel3

JPanel4

JPanel5


تمرين عملي: قم بإنشاء الواجهة الرسومية السابقة باستخدام البرنامج التعليمي [http://www.netbeans.org/kb/trails/matisse.html].


5.12.3. أحداث واجهة المستخدم الرسومية

قراءة موصى بها: الفصل [واجهات المستخدم الرسومية] في [ref2].

سنقوم بمعالجة النقر على زر [jButtonSalaire]. لإنشاء الطريقة التي تعالج هذا الحدث، يمكننا اتباع الخطوات التالية:

يتم إنشاء معالج النقر على زر [JButtonSalaire]:

1
2
3
    private void jButtonSalaireActionPerformed(java.awt.event.ActionEvent evt) {
       // TODO add your handling code here:
}

يتم أيضًا إنشاء كود Java الذي يربط الطريقة السابقة بالنقر على زر [JButtonSalaire]:

1
2
3
4
5
6
    jButtonSalaire.setText("Salaire");
    jButtonSalaire.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButtonSalaireActionPerformed(evt);
      }
});

تحدد الأسطر 2-5 أن النقر (evt من النوع ActionPerformed) على الزر [jButtonSalaire] (السطر 2) يجب أن تتم معالجته بواسطة الطريقة [jButtonSalaireActionPerformed] (السطر 4).

سنقوم أيضًا بمعالجة حدث [caretUpdate] (حركة المؤشر) في حقل الإدخال [jTextFieldHT]. لإنشاء معالج لهذا الحدث، نتبع نفس الخطوات السابقة:

يتم إنشاء معالج الحدث [caretUpdate] في حقل الإدخال [jTextFieldHT]:

  private void jTextFieldHTCaretUpdate(javax.swing.event.CaretEvent evt) {                                         
 ...
  }

يتم أيضًا إنشاء كود Java الذي يربط الطريقة السابقة بحدث [caretUpdate] في حقل النص [jTextFieldHT]:

1
2
3
4
5
    jTextFieldHT.addCaretListener(new javax.swing.event.CaretListener() {
      public void caretUpdate(javax.swing.event.CaretEvent evt) {
        jTextFieldHTCaretUpdate(evt);
      }
});

تشير الأسطر 1-4 إلى أن الحدث [caretUpdate] (السطر 2) على الزر [jTextFieldHT] (السطر 1) يجب أن تتم معالجته بواسطة الطريقة [jTextFieldHTCaretUpdate] (السطر 3).

5.12.4. تهيئة واجهة المستخدم الرسومية

لنعد إلى بنية تطبيقنا:

تحتاج طبقة [ui] إلى مرجع إلى طبقة [business]. دعونا نتذكر كيف تم الحصول على هذا المرجع في تطبيق وحدة التحكم:

1
2
3
    // instantiation layer [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
IMetier metier = (IMetier) ctx.getBean("metier");

الطريقة هي نفسها في تطبيق واجهة المستخدم الرسومية. عند تهيئة تطبيق واجهة المستخدم الرسومية، يجب أيضًا تهيئة مرجع [IMetier metier] من السطر 3 أعلاه. الرمز الذي تم إنشاؤه لواجهة المستخدم الرسومية هو حاليًا كما يلي:

package ui.swing;

...
public class PamJFrame extends javax.swing.JFrame {

   /** Creates new form PamJFrame */
  public PamJFrame() {
    initComponents();
  }

  /** This method is called from within the constructor to
   * initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is
   * always regenerated by the Form Editor.
   */
   // <editor-fold defaultstate="collapsed" desc=" Generated Code ">
  private void initComponents() {
...
  }// </editor-fold>

  private void jTextFieldHTCaretUpdate(javax.swing.event.CaretEvent evt) {                                         
 ...
  }                                        

  private void jButtonSalaireActionPerformed(java.awt.event.ActionEvent evt) {                                               
...
  }                                              

  public static void main(String args[]) {
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        new PamJFrame().setVisible(true);
      }
    });
  }

  // Variables declaration - do not modify
  private javax.swing.JButton jButtonSalaire;
...
   // End of variables declaration

}
  • الأسطر 29–35: الطريقة الثابتة [main] التي تطلق التطبيق
  • السطر 32: يتم إنشاء مثيل لواجهة المستخدم الرسومية [PamJFrame] وإظهاره.
  • الأسطر 7-9: منشئ واجهة المستخدم الرسومية.
  • السطر 8: استدعاء الطريقة [initComponents] المحددة في السطر 17. يتم إنشاء هذه الطريقة تلقائيًا بناءً على العمل المنجز في وضع [Design]. لا تقم بتعديلها.
  • السطر 21: الطريقة التي ستتولى تحريك مؤشر الإدخال في حقل [jTextFieldHT]
  • السطر 25: الطريقة التي ستتولى معالجة النقر على زر [jButtonSalaire]

لإضافة التهيئات الخاصة بنا إلى الكود السابق، يمكننا المتابعة على النحو التالي:

  /** Creates new form PamJFrame */
  public PamJFrame() {
    initComponents();
    doMyInit();
  }

...

   // instance variables
  private IMetier metier=null;
  private List<Employe> employes=null;
  private String[] employesCombo=null;
  private double heuresTravaillées=0;

   // proprietary initializations
  public void doMyInit(){
     // init context
    try{
       // instantiation layer [metier]
...
     // list of employees
...
    }catch (PamException ex){
     // the exception message is placed in [jTextAreaStatus]
...
     // return
      return;
    }
     // salary button disabled
...
     // jScrollPane1 hidden
...
     // spinner days worked
    jSpinnerJT.setModel(new SpinnerNumberModel(0,0,31,1));
     // combobox employees
    employesCombo=new String[employes.size()];
    int i=0;
    for(Employe employe : employes){
      employesCombo[i++]=employe.getPrenom()+" "+employe.getNom();
    }
    jComboBoxEmployes.setModel(new DefaultComboBoxModel(employesCombo));
}
  • السطر 4: نستدعي طريقة مخصصة لتنفيذ عمليات التهيئة الخاصة بنا. يتم تعريف هذه العمليات بواسطة الكود الموجود في الأسطر 10–42

السؤال: باستخدام التعليقات كدليل، أكمل الكود الخاص بإجراء [doMyInit].


5.12.5. معالجات الأحداث


السؤال: اكتب الأسلوب [jTextFieldHTCaretUpdate]. يجب أن يضمن هذا الأسلوب أنه إذا لم تكن البيانات الموجودة في الحقل [jTextFieldHT] عددًا حقيقيًا >=0، فيجب تعطيل الزر [jButtonSalaire].



السؤال: اكتب الأسلوب [jButtonSalaireActionPerformed]، الذي يجب أن يعرض كشف الراتب للموظف المحدد في [jComboBoxEmployes].


5.12.6. تشغيل واجهة المستخدم الرسومية

لتشغيل واجهة المستخدم الرسومية، قم بتعديل تكوين [Run] للمشروع:

  • في [1]، أدخل فئة واجهة المستخدم الرسومية

يجب أن يكون المشروع كاملاً مع ملفات التكوين الخاصة به (persistence.xml، spring-config-metier-dao.xml) وفئة واجهة المستخدم الرسومية. قم بتشغيل نظام إدارة قواعد البيانات المستهدف قبل تشغيل المشروع.

نحن مهتمون بالبنية التالية حيث يتم الآن تنفيذ طبقة JPA بواسطة EclipseLink:

5.13.1. مشروع NetBeans

يتم إنشاء مشروع NetBeans الجديد عن طريق نسخ المشروع السابق:

  • في [1]: بعد النقر بزر الماوس الأيمن على مشروع Hibernate، حدد "نسخ"
  • باستخدام الزر [2]، حدد المجلد الأصلي للمشروع الجديد. يظهر اسم المجلد في [3].
  • في [4]، قم بتسمية المشروع الجديد
  • في [5]، اسم مجلد المشروع
  • في [1]، تم إنشاء المشروع الجديد. يحمل نفس اسم المشروع الأصلي،
  • في [2] و[3]، قم بتغيير اسمه إلى [mv-pam-spring-eclipselink].

يجب تعديل المشروع في موضعين لتكييفه مع طبقة JPA / EclipseLink الجديدة:

  1. في [4]، يجب تعديل ملفات تكوين Spring. هذا هو المكان الذي توجد فيه تكوينات طبقة JPA.
  2. في [5]، يجب تعديل مكتبات المشروع: يجب استبدال مكتبات Hibernate بمكتبات EclipseLink.

لنبدأ بالنقطة الأخيرة. سيكون ملف [pom.xml] للمشروع الجديد كما يلي:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>istia.st</groupId>
  <artifactId>mv-pam-spring-eclipselink</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
 
  <name>mv-pam-spring-eclipselink</name>
  <url>http://maven.apache.org</url>
  <repositories>
    <repository>
      <url>http://repo1.maven.org/maven2/</url>
      <id>swing-layout</id>
      <layout>default</layout>
      <name>Repository for library Library[swing-layout]</name>
    </repository>
    <repository>
      <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
      <id>eclipselink</id>
      <layout>default</layout>
      <name>Repository for library Library[eclipselink]</name>
    </repository>    
  </repositories>
 
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
 
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>commons-dbcp</groupId>
      <artifactId>commons-dbcp</artifactId>
      <version>1.2.2</version>
    </dependency>
    <dependency>
      <groupId>commons-pool</groupId>
      <artifactId>commons-pool</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>eclipselink</artifactId>
      <version>2.3.0</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>javax.persistence</artifactId>
      <version>2.0.3</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
    <dependency>
      <groupId>org.swinglabs</groupId>
      <artifactId>swing-layout</artifactId>
      <version>1.0.3</version>
    </dependency>
  </dependencies>
</project>
  • الأسطر 73–82: التبعيات لتنفيذ EclipseLink JPA،
  • الأسطر 19–24: مستودع Maven لـ EclipseLink.

يجب تعديل ملفات تكوين Spring للإشارة إلى أن تطبيق JPA قد تغير. في كلا الملفين، يتغير فقط القسم الذي يهيئ طبقة JPA. على سبيل المثال، في [spring-config-metier-dao.xml] لدينا:

<?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 -->
   <!- - DAO -->
  <bean id="employeDao" class="dao.EmployeDao" />
  <bean id="indemniteDao" class="dao.IndemniteDao" />
  <bean id="cotisationDao" class="dao.CotisationDao" />
   <!-- business -->
  <bean id="metier" class="metier.Metier">
    <property name="employeDao" ref="employeDao"/>
    <property name="indemniteDao" ref="indemniteDao"/>
    <property name="cotisationDao" ref="cotisationDao"/>  
  </bean>

   <!-- 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="showSql" value="true" />
    -->
        <property name="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
        <property name="generateDdl" value="true" />
   <!--
        <property name="generateDdl" value="true" />
        -->
      </bean>
    </property>
    <property name="loadTimeWeaver">
      <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    </property>
  </bean>

   <!-- data source 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>
....  
</beans>

تقوم الأسطر 19–36 بتكوين طبقة JPA. يتم استخدام Hibernate لتنفيذ JPA (السطر 22). بالإضافة إلى ذلك، فإن قاعدة البيانات المستهدفة هي [dbpam_hibernate] (السطر 41).

للتبديل إلى تطبيق JPA/EclipseLink، يتم استبدال الأسطر 19–35 أعلاه بالأسطر التالية:

  <!-- 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.EclipseLinkJpaVendorAdapter">
        <!--
          <property name="showSql" value="true" />
  -->
        <property name="databasePlatform" value="org.eclipse.persistence.platform.database.MySQLPlatform" />
        <!--
        <property name="generateDdl" value="true" />
        -->
      </bean>
    </property>
    <property name="loadTimeWeaver">
      <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    </property>
</bean>
  • السطر 5: تطبيق JPA المستخدم هو EclipseLink
  • السطر 9: تحدد الخاصية databasePlatform نظام إدارة قواعد البيانات المستهدف، وهو في هذه الحالة MySQL
  • السطر 11: لإنشاء جداول قاعدة البيانات عند إنشاء مثيل لطبقة JPA. هنا، تم تعليق الخاصية.
  • السطر 7: لعرض عبارات SQL الصادرة عن طبقة JPA على وحدة التحكم. هنا، تم تعليق الخاصية.

بالإضافة إلى ذلك، تصبح قاعدة البيانات المستهدفة [dbpam_eclipselink] (السطر 4 أدناه):

1
2
3
4
5
6
7
8
9
<!-- data source 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_eclipselink" />
    <property name="username" value="root" />
<!--
    <property name="password" value="" />
-->
  </bean>

5.13.2. تشغيل الاختبارات

قبل اختبار التطبيق بأكمله، يُنصح بالتحقق من نجاح اختبارات JUnit مع تطبيق JPA الجديد. قبل تشغيلها، سنبدأ بحذف الجداول من قاعدة البيانات. للقيام بذلك، في علامة التبويب [Runtime] في NetBeans، قم بإنشاء اتصال بقاعدة البيانات dbpam_eclipselink / MySQL5، إذا لزم الأمر. بمجرد الاتصال بقاعدة البيانات dbpam_eclipselink / MySQL5، يمكنك المضي قدمًا في حذف الجداول كما هو موضح أدناه:

  • [1]: قبل الحذف
  • [2]: بعد الحذف

بمجرد الانتهاء من ذلك، يمكنك تشغيل الاختبار الأول على طبقة [DAO]: InitDB، الذي يقوم بتعبئة قاعدة البيانات. للتأكد من أن الجداول التي تم حذفها مسبقًا يتم إعادة إنشاؤها بواسطة التطبيق، تأكد من أن السطر التالي موجود في تكوين Spring JPA / EclipseLink:

        <property name="generateDdl" value="true" />

موجودًا وغير معلق عليه.

نقوم ببناء المشروع ثم نُشغّل اختبار [JUnit InitDB]:

  • في [1]، يتم تشغيل اختبار InitDB.
  • في [2]، يفشل الاختبار. يتم إلقاء الاستثناء بواسطة Spring وليس بواسطة الاختبار الذي فشل.

سبب الخطأ: org.springframework.beans.factory.BeanCreationException: خطأ في إنشاء bean باسم 'entityManagerFactory' المحدد في مورد مسار الفئة [spring-config-DAO.xml]: فشل استدعاء طريقة init؛ الاستثناء المتداخل هو java.lang.IllegalStateException: يجب بدء التشغيل باستخدام وكيل Java لاستخدام InstrumentationLoadTimeWeaver. انظر وثائق Spring.

يشير Spring إلى وجود مشكلة في التكوين. الرسالة غير واضحة. تم شرح سبب الاستثناء في القسم 3.1.9 من [ref1]. لكي يعمل تكوين Spring/EclipseLink، يجب تشغيل JVM الذي يقوم بتشغيل التطبيق باستخدام معلمة محددة، وهي وكيل Java. تنسيق هذه المعلمة هو كما يلي:

-javaagent:C:\...\spring-agent.jar

[spring-agent.jar] هو وكيل Java المطلوب من قبل JVM لإدارة تكوين Spring/EclipseLink.

عند تشغيل مشروع، يمكن تمرير معلمات إلى JVM:

  • في [1]، يمكنك الوصول إلى خصائص المشروع
  • في [2]، خصائص التشغيل
  • في [3]، قم بتمرير المعلمة -javaagent إلى JVM

5.13.3. InitDB

الآن نحن جاهزون لاختبار [InitDB] مرة أخرى. هذه المرة، كانت النتائج كما يلي:

  • في [1]، نجح الاختبار
  • في [2]، في علامة التبويب [Services]، نقوم بتحديث اتصال NetBeans بقاعدة البيانات [dbpam_eclipselink]
  • في [3]، تم إنشاء أربعة جداول
  • في [5]، نعرض محتويات جدول [employees]
  • في [6]، النتيجة.

5.13.4. JUnitDao

قد يفشل تنفيذ فئة الاختبار [JUnitDao]، على الرغم من نجاحها مع تطبيق JPA/Hibernate. لفهم السبب، دعونا نحلل مثالاً.

الطريقة التي يتم اختبارها هي طريقة IndemniteDao.create التالية:

package dao;

...
@Transactional(propagation=Propagation.REQUIRED)
public class IndemniteDao implements IIndemniteDao{

  @PersistenceContext
  private EntityManager em;

   // manufacturer
  public IndemniteDao() {
  }

   // create an allowance
  public Indemnite create(Indemnite indemnite) {
    try{
      em.persist(indemnite);
    }catch(Throwable th){
      throw new PamException(th,31);
    }
    return indemnite;
  }

...
}
  • الأسطر 15–22: الطريقة قيد الاختبار

طريقة الاختبار هي كما يلي:


package dao;
 
...
 
public class JUnitDao {
 
// layers DAO
  static private IEmployeDao employeDao;
  static private IIndemniteDao indemniteDao;
  static private ICotisationDao cotisationDao;
 
  @BeforeClass
  public static void init() {
    // log
    log("init");
    // application configuration
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-DAO.xml");
    // layers DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
  }
 
  @Before()
  public void clean() {
    // empty the base
    for (Employe employe : employeDao.findAll()) {
      employeDao.destroy(employe);
    }
    for (Cotisation cotisation : cotisationDao.findAll()) {
      cotisationDao.destroy(cotisation);
    }
    for (Indemnite indemnite : indemniteDao.findAll()) {
      indemniteDao.destroy(indemnite);
    }
  }
 
  // logs
  private static void log(String message) {
    System.out.println("----------- " + message);
  }
 
  // tests
….
  @Test
  public void test05() {
    log("test05");
    // we create two allowances with the same index
    // violates index uniqueness constraint
    boolean erreur = true;
    Indemnite indemnite1 = null;
    Indemnite indemnite2 = null;
    Throwable th = null;
    try {
      indemnite1 = indemniteDao.create(new Indemnite(1, 1.93, 2, 3, 12));
      indemnite2 = indemniteDao.create(new Indemnite(1, 1.93, 2, 3, 12));
      erreur = false;
    } catch (PamException ex) {
      th = ex;
      // checks
      Assert.assertEquals(31, ex.getCode());
    } catch (Throwable th1) {
      th = th1;
    }
    // checks
    Assert.assertTrue(erreur);
    // exception chain
    System.out.println("Chaîne des exceptions --------------------------------------");
    System.out.println(th.getClass().getName());
    while (th.getCause() != null) {
      th = th.getCause();
      System.out.println(th.getClass().getName());
    }
    // the 1st allowance had to be continued
    Indemnite indemnite = indemniteDao.find(indemnite1.getId());
    // check
    Assert.assertNotNull(indemnite);
    Assert.assertEquals(1, indemnite.getIndice());
    Assert.assertEquals(1.93, indemnite.getBaseHeure(), 1e-6);
    Assert.assertEquals(2, indemnite.getEntretienJour(), 1e-6);
    Assert.assertEquals(3, indemnite.getRepasJour(), 1e-6);
    Assert.assertEquals(12, indemnite.getIndemnitesCP(), 1e-6);
    // the second indemnity should not have persisted
    List<Indemnite> indemnites = indemniteDao.findAll();
    int nbIndemnites = indemnites.size();
    Assert.assertEquals(nbIndemnites, 1);
  }
 
...
}

السؤال: اشرح وظيفة الاختبار test05 ووضح النتائج المتوقعة.


النتائج التي تم الحصول عليها باستخدام طبقة JPA/Hibernate هي كما يلي:

----------- test05
4 juin 2010 16:45:43 org.hibernate.util.JDBCExceptionReporter logExceptions
ATTENTION: SQL Error: 1062, SQLState: 23000
4 juin 2010 16:45:43 org.hibernate.util.JDBCExceptionReporter logExceptions
GRAVE: Duplicate entry '1' for key 2
Chaîne des exceptions --------------------------------------
exception.PamException
javax.persistence.EntityExistsException
org.hibernate.exception.ConstraintViolationException
com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException

اجتاز الاختبار، مما يعني أن التأكيدات تم التحقق منها ولم يتم إلقاء أي استثناء من طريقة الاختبار.


السؤال: اشرح ما حدث.


النتائج التي تم الحصول عليها باستخدام طبقة JPA/EclipseLink هي كما يلي:

----------- test05
[EL Warning]: 2010-06-04 16:48:26.421--UnitOfWork(749304)--Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.0.0.v20091127-r5931): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry '1' for key 2
Error Code: 1062
Call: INSERT INTO INDEMNITES (ID, ENTRETIEN_JOUR, REPAS_JOUR, INDICE, INDEMNITES_CP, BASE_HEURE, VERSION) VALUES (?, ?, ?, ?, ?, ?, ?)
        bind => [108, 2.0, 3.0, 1, 12.0, 1.93, 1]
Query: InsertObjectQuery(jpa.Indemnite[id=108,version=1,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0])
Chaîne des exceptions --------------------------------------
org.springframework.transaction.TransactionSystemException
javax.persistence.RollbackException
org.eclipse.persistence.exceptions.DatabaseException
com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException

كما هو الحال مع Hibernate سابقًا، ينجح الاختبار، مما يعني أن التأكيدات تم التحقق منها ولم يتم إلقاء أي استثناء من طريقة الاختبار.


السؤال: اشرح ما حدث.



سؤال: من هذين المثالين، ما الذي يمكننا استنتاجه بشأن قابلية تبادل تطبيقات JPA؟ هل الأمر كامل هنا؟


5.13.5. الاختبارات الأخرى

بمجرد اختبار طبقة [DAO] واعتبارها صحيحة، يمكننا الانتقال إلى اختبار طبقة [الأعمال] والمشروع نفسه في نسخته النصية أو الرسومية. لا يؤثر تغيير تطبيق JPA على طبقات [الأعمال] و[واجهة المستخدم]؛ لذلك، إذا كانت هذه الطبقات تعمل مع Hibernate، فستعمل مع EclipseLink مع بعض الاستثناءات: يوضح المثال السابق أن الاستثناءات التي تطلقها طبقات [DAO] قد تختلف. وبالتالي، في حالة الاختبار، يرمي Spring / JPA / Hibernate استثناء [PamException]، وهو استثناء خاص بتطبيق [pam]، في حين يرمي Spring / JPA / EclipseLink استثناء [TransactionSystemException]، وهو استثناء من إطار عمل Spring. إذا كانت طبقة [ui]، في حالة الاختبار، تتوقع استثناء [PamException] لأنها بُنيت باستخدام Hibernate، فلن تعمل بعد ذلك عند التبديل إلى EclipseLink.

5.13.6. العمل المطلوب


مهمة عملية: إعادة اختبار تطبيقات وحدة التحكم و Swing باستخدام أنظمة إدارة قواعد البيانات المختلفة: MySQL5 و Oracle XE و SQL Server.