Skip to content

3. JPA في بنية متعددة الطبقات

لدراسة API JPA، استخدمنا بنية الاختبار التالية:

كانت برامج الاختبار الخاصة بنا عبارة عن تطبيقات وحدة التحكم التي تستعلم مباشرة عن الطبقة JPA. اكتشفنا في هذه المناسبة الطرق الرئيسية للطبقة JPA. كنا في بيئة تُعرف باسم "Java SE" (الإصدار القياسي). يعمل JPA في كل من بيئة Java SE وبيئة Java EE5 (الإصدار المؤسسي).

الآن بعد أن أصبحنا نتمتع بقدر من الإلمام بكل من تكوين الجسر العلائقي/الكائني واستخدام أساليب طبقة JPA، نعود إلى بنية متعددة الطبقات أكثر تقليدية:

سيتم الوصول إلى الطبقة [JPA] عبر بنية ذات طبقتين [metier] و [dao]. سيتم استخدام إطار عمل Spring [7]، ثم الحاوية EJB3 من JBoss [8] لربط هذه الطبقات ببعضها البعض.

ذكرنا أعلاه أن JPA متاح في بيئات SE و EE5. تقدم بيئة Java EE5 العديد من الخدمات في مجال الوصول إلى البيانات الدائمة، لا سيما مجموعات الاتصال ومديري المعاملات، ... وقد يكون من المفيد للمطور الاستفادة من هذه الخدمات. بيئة Java EE5 ليست منتشرة بعد (مايو 2007). وهي موجودة حاليًا على خادم التطبيقات Sun Application Server 9.x (Glassfish). خادم التطبيقات هو في الأساس خادم لتطبيقات الويب. إذا قمنا بإنشاء تطبيق رسومي مستقل من نوع Swing، فلن نتمكن من الاستفادة من بيئة EE والخدمات التي توفرها. وهذا يمثل مشكلة. بدأنا نرى بيئات "مستقلة" من نوع EE و c.a.d. يمكن استخدامها خارج خادم التطبيقات. هذا هو الحال بالنسبة لـ JBoss و EJB3 التي سنستخدمها في هذا المستند.

في بيئة EE5، يتم تنفيذ الطبقات بواسطة كائنات تسمى EJB (Enterprise Java Bean). في الإصدارات السابقة من EE، يُعرف عن EJB (EJB 2.x) صعوبة تنفيذها واختبارها، وأحيانًا ضعف أدائها. يتم التمييز بين "entity" EJB2.x و"session" EJB2.x. باختصار، فإن "entity" EJB2.x هي صورة لصف في جدول قاعدة بيانات، و"session" EJB2.x هي كائن يُستخدم لتنفيذ طبقات [metier]، [dao] في بنية متعددة الطبقات. أحد الانتقادات الرئيسية الموجهة إلى الطبقات التي تم تنفيذها باستخدام EJB هو أنها لا يمكن استخدامها إلا داخل حاويات EJB، وهي خدمة تقدمها بيئة EE. وهذا يجعل الاختبارات الفردية مشكلة. وبالتالي، في المخطط أعلاه، ستتطلب الاختبارات الفردية للطبقات [metier] و [dao] التي تم إنشاؤها باستخدام EJB إعداد خادم تطبيقات، وهي عملية مرهقة إلى حد ما ولا تشجع المطور حقًا على إجراء الاختبارات بشكل متكرر.

نشأ إطار عمل Spring كرد فعل على تعقيد بيئات EJB2. يوفر Spring في بيئة SE عددًا كبيرًا من الخدمات التي عادةً ما توفرها بيئات EE. وهكذا، في الجزء "استمرارية البيانات" الذي يهمنا هنا، يوفر Spring مجموعات الاتصال ومديري المعاملات التي تحتاجها التطبيقات. وقد ساهم ظهور Spring في تعزيز ثقافة الاختبارات الفردية، التي أصبحت فجأة أسهل بكثير في التنفيذ. يسمح Spring بتنفيذ طبقات التطبيق باستخدام كائنات Java التقليدية (POJO، Plain Old/Ordinary Java Object)، مما يتيح إعادة استخدامها في سياق آخر. وأخيرًا، يدمج العديد من أدوات الجهات الخارجية بطريقة شفافة إلى حد كبير، لا سيما أدوات الاستمرارية مثل Hibernate وIbatis، ...

تم تصميم Java EE5 لتصحيح أوجه القصور في المواصفات السابقة EE. أصبحت EJB و 2.x هي EJB3. وهذه هي POJOs الموسومة بتعليقات توضيحية تجعلها كائنات خاصة عندما تكون داخل حاوية EJB3. في هذا الحاوية، سيتمكن EJB3 من الاستفادة من خدمات الحاوية (مجموعة الاتصالات، مدير المعاملات، ...). خارج الحاوية EJB3، يصبح EJB3 كائن Java عادي. يتم تجاهل تعليقاته التوضيحية EJB.

فيما سبق، قمنا بتمثيل Spring و JBoss EJB3 كبنية تحتية (إطار عمل) محتملة لهندستنا متعددة الطبقات. هذه البنية التحتية هي التي ستوفر الخدمات التي نحتاجها: تجمع اتصالات ومدير معاملات.

  • مع Spring، سيتم تنفيذ الطبقات باستخدام POJOs. وستتمكن هذه من الوصول إلى خدمات Spring (مجموعة الاتصالات، مدير المعاملات) عن طريق حقن التبعيات في هذه POJOs: عند إنشاء هذه، يقوم Spring بحقنها بمراجع للخدمات التي ستحتاجها.
  • JBoss EJB3 هو حاوية EJB يمكنها العمل خارج خادم التطبيق. مبدأ عملها (بالنسبة للمطور) مشابه للمبدأ الموصوف لـ Spring. لن نجد سوى القليل من الاختلافات.

سنختتم هذا المستند بمثال لتطبيق ويب ثلاثي الطبقات، بسيط ولكنه مع ذلك تمثيلي:

3.1. مثال 1: Spring / JPA مع كيان Personne

نأخذ الكيان Personne الذي تمت دراسته في الفقرة 2.1، وندمجه في بنية متعددة الطبقات حيث يتم دمج الطبقات باستخدام Spring ويتم تنفيذ طبقة الاستمرارية بواسطة Hibernate.

يُفترض هنا أن القارئ لديه معرفة أساسية بـ Spring. إذا لم يكن الأمر كذلك، يمكن قراءة الوثيقة التالية التي تشرح مفهوم حقن التبعيات الذي يمثل جوهر Spring:

[ref3]: Spring Ioc (Inversion Of Control) [http://tahe.developpez.com/java/springioc].

3.1.1. مشروع Eclipse / Spring / Hibernate على

مشروع Eclipse هو التالي:

  • في [1]: مشروع Eclipse. يمكن العثور عليه في [6] ضمن أمثلة البرنامج التعليمي [5]. سنقوم باستيراده.
  • في [2]: أكواد Java للطبقات المعروضة في الحزم:
    • [entites]: حزمة الكيانات JPA
    • [dao]: طبقة الوصول إلى البيانات - تعتمد على الطبقة JPA
    • [service]: طبقة خدمات أكثر منها طبقة مهنية. سيتم فيها استخدام خدمة معاملات الحاويات.
    • [tests]: تضم برامج الاختبار.
  • في [3]: تجمع مكتبة [jpa-spring] ملفات jar الضرورية لـ Spring (انظر أيضًا [7] و [8]).
  • في [4]: المجلد [conf] يجمع ملفات تكوين Spring لكل ملف من ملفات SGBD المستخدمة في هذا البرنامج التعليمي.

3.1.2. الكيانات JPA

لا يوجد سوى كيان واحد مُدار هنا، وهو الكيان Personne الذي تمت دراسته في الفقرة 2.1، والذي نذكر تكوينه أدناه:


package entites;

...
@Entity
@Table(name="jpa01_hb_personne")
public class Personne {

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

    // المنشئات
    public Personne() {
    }

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
...
    }

    // toString
    public String toString() {
        return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
                getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
                        .format(getDatenaissance()), isMarie(), getNbenfants());
    }

    // مُستردات ومُعيّنات
...
}

3.1.3. الطبقة [dao]

تقدم الطبقة [dao] الواجهة IDao التالية:


package dao;

import java.util.List;

import entites.Personne;

public interface IDao {
    // الحصول على شخص عبر معرّفه
    public Personne getOne(Integer id);

    // الحصول على جميع الأشخاص
    public List<Personne> getAll();

    // حفظ شخص
    public Personne saveOne(Personne personne);

    // تحديث شخص
    public Personne updateOne(Personne personne);

    // حذف شخص عبر معرّفه
    public void deleteOne(Integer id);

    // الحصول على الأشخاص الذين يتطابق اسمهم مع نموذج
    public List<Personne> getAllLike(String modele);

}

التنفيذ [Dao] لهذه الواجهة هو كما يلي:


package dao;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import entites.Personne;

public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

    // حذف شخص عبر معرّفه
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }

    @SuppressWarnings("unchecked")
    // الحصول على جميع الأشخاص
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    @SuppressWarnings("unchecked")
    // الحصول على الأشخاص الذين يتطابق اسمهم مع نموذج
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // الحصول على شخص عبر معرّفه
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // حفظ شخص
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // تحديث شخص
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }

}
  • أولاً، نلاحظ بساطة التنفيذ [Dao]. ويرجع ذلك إلى استخدام الطبقة JPA التي تقوم بمعظم عمل الوصول إلى البيانات.
  • السطر 10: تنفذ الفئة [Dao] واجهة [IDao]
  • السطر 13: الكائن من النوع [EntityManager] الذي سيتم استخدامه لمعالجة سياق الاستمرارية JPA. لأغراض اللغة، سنخلط بينه وبين سياق الاستمرارية نفسه في بعض الأحيان. سيحتوي سياق الاستمرارية على كيانات Personne.
  • السطر 12: لم يتم تهيئة الحقل [EntityManager em] في أي مكان في الكود. سيتم تهيئته عند بدء تشغيل التطبيق بواسطة Spring. إن التعليق التوضيحي JPA @PersistenceContext في السطر 12 هو الذي يطلب من Spring حقن مدير سياق الاستمرارية في em.
  • الأسطر 26-28: يتم الحصول على قائمة بجميع الأشخاص من خلال استعلام JPQL.
  • الأسطر 32-35: يتم الحصول على قائمة بجميع الأشخاص الذين لديهم اسم يتوافق مع نموذج معين من خلال استعلام JPQL.
  • الأسطر 38-40: يتم الحصول على الشخص الذي يحمل هذا المعرف من خلال طريقة find في API JPA. تعرض مؤشر null إذا كان الشخص غير موجود.
  • الأسطر 43-46: يتم حفظ بيانات الشخص بشكل دائم باستخدام طريقة persist في API JPA. هذه الطريقة تعمل على حفظ بيانات الشخص بشكل دائم.
  • الأسطر 49-51: يتم تحديث شخص ما بواسطة طريقة merge في API JPA. لا يكون لهذه الطريقة معنى إلا إذا كان الشخص الذي تم تحديثه بهذه الطريقة منفصلاً من قبل. تجعل هذه الطريقة الشخص الذي تم إنشاؤه بهذه الطريقة ثابتًا.
  • الأسطر 16-22: يتم حذف الشخص الذي يتم تمرير معرفه كمعلمة في خطوتين:
    • السطر 17: يتم البحث عنها في سياق الاستمرارية
    • الأسطر 18-20: إذا لم يتم العثور عليها، يتم إلقاء استثناء برمز خطأ 2
    • السطر 21: إذا تم العثور عليها، يتم إزالتها من سياق الاستمرارية باستخدام طريقة remove في API JPA.
  • ما لا يظهر في الوقت الحالي هو أن كل طريقة سيتم تنفيذها ضمن معاملة تبدأها الطبقة [service].

التطبيق له نوع استثناء خاص به يسمى [DaoException]:


package dao;

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

    // رمز الخطأ
    private int code;

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

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

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

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

    // getter و setter

    public int getCode() {
        return code;
    }

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

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

3.1.4. الطبقة [metier / service]

تقدم الطبقة [service] الواجهة [IService] التالية:


package service;

import java.util.List;

import entites.Personne;

public interface IService {
    // الحصول على شخص عبر معرّفه
    public Personne getOne(Integer id);

    // الحصول على جميع الأشخاص
    public List<Personne> getAll();

    // حفظ شخص
    public Personne saveOne(Personne personne);

    // تحديث شخص
    public Personne updateOne(Personne personne);

    // حذف شخص عبر معرّفه
    public void deleteOne(Integer id);

    // الحصول على الأشخاص الذين يتطابق اسمهم مع نموذج
    public List<Personne> getAllLike(String modele);

    // حذف عدة أشخاص في وقت واحد
    public void deleteArray(Personne[] personnes);

    // حفظ عدة أشخاص في وقت واحد
    public Personne[] saveArray(Personne[] personnes);

    // تحديث عدة أشخاص في وقت واحد
    public Personne[] updateArray(Personne[] personnes);

}
  • الأسطر 8-24: تضم واجهة [IService] أساليب واجهة [IDao]
  • السطر 27: تسمح طريقة [deleteArray] بحذف مجموعة من الأشخاص ضمن معاملة واحدة: إما يتم حذف جميع الأشخاص أو لا يتم حذف أي منهم.
  • السطران 30 و33: طرق مشابهة لـ [deleteArray] لحفظ (السطر 30) أو تحديث (السطر 33) مجموعة من الأشخاص ضمن معاملة.

التنفيذ [Service] للواجهة [IService] هو كما يلي:


package service;

...

// تتم جميع طرق الفئة في معاملة واحدة
@Transactional
public class Service implements IService {

    // طبقة [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

    // حذف عدة أشخاص في وقت واحد
    public void deleteArray(Personne[] personnes) {
        for (Personne p : personnes) {
            dao.deleteOne(p.getId());
        }
    }

    // حذف شخص عبر معرّفه
    public void deleteOne(Integer id) {
        dao.deleteOne(id);
    }

    // الحصول على جميع الأشخاص
    public List<Personne> getAll() {
        return dao.getAll();
    }

    // الحصول على الأشخاص الذين يتطابق اسمهم مع نموذج
    public List<Personne> getAllLike(String modele) {
        return dao.getAllLike(modele);
    }

    // الحصول على شخص عبر معرّفه
    public Personne getOne(Integer id) {
        return dao.getOne(id);
    }

    // حفظ عدة أشخاص في وقت واحد
    public Personne[] saveArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.saveOne(personnes[i]);
        }
        return personnes2;
    }

    // حفظ شخص واحد
    public Personne saveOne(Personne personne) {
        return dao.saveOne(personne);
    }

    // تحديث عدة أشخاص في وقت واحد
    public Personne[] updateArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.updateOne(personnes[i]);
        }
        return personnes2;
    }

    // تحديث شخص واحد
    public Personne updateOne(Personne personne) {
        return dao.updateOne(personne);
    }

}
  • السطر 6: تشير تعليمة Spring @Transactional إلى أن جميع أساليب الفئة يجب أن تُنفَّذ ضمن معاملة. سيتم بدء معاملة قبل بدء تنفيذ الأسلوب وإغلاقها بعد التنفيذ. إذا حدثت استثناء من النوع [RuntimeException] أو مشتق منه أثناء تنفيذ الأسلوب، فإن التراجع التلقائي يلغي المعاملة بأكملها، وإلا فإن الالتزام التلقائي يصادق عليها. تجدر الإشارة إلى أن كود Java لا يحتاج إلى الاهتمام بالمعاملات. فهي تدار بواسطة Spring.
  • السطر 10: مرجع إلى الطبقة [dao]. سنرى لاحقًا أن Spring يقوم بتهيئة هذا المرجع عند بدء تشغيل التطبيق.
  • تكتفي طرق [Service] باستدعاء طرق واجهة [IDao dao] في السطر 10. نترك للقارئ الاطلاع على الكود. لا توجد صعوبات خاصة.
  • ذكرنا سابقًا أن كل طريقة في [Service] تُنفَّذ في معاملة. وترتبط هذه المعاملة بخيط تنفيذ الطريقة. وفي هذا الخيط، تُنفَّذ طرق الطبقة [dao]. وستُربط هذه الطرق تلقائيًا بمعاملة خيط التنفيذ. على سبيل المثال، يتم تنفيذ الطريقة [deleteArray] (السطر 21) N مرات لطريقة [deleteOne] من الطبقة [dao]. وستتم عمليات التنفيذ هذه N مرات ضمن مؤشر ترابط تنفيذ الطريقة [deleteArray]، وبالتالي ضمن نفس المعاملة. كما سيتم إما المصادقة عليها جميعًا (commit) إذا سارت الأمور على ما يرام أو إلغاؤها جميعًا (rollback) في حالة حدوث استثناء في إحدى عمليات التنفيذ N للطريقة [deleteOne] في الطبقة [dao].

3.1.5. تكوين الطبقات

يتم تكوين الطبقات [service] و [dao] و [JPA] بواسطة الملفين المذكورين أعلاه: [META-INF/persistence.xml] و [spring-config.xml]. يجب أن يكون الملفان موجودين في classpath للتطبيق، وهو ما يفسر وجودهما في مجلد [src] لمشروع Eclipse. اسم الملف [spring-config.xml] حر.

persistence.xml


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.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_1_0.xsd">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />
</persistence>
  • السطر 4: يعلن الملف عن وحدة استمرارية تسمى jpa تستخدم معاملات "محلية"، c.a.d. غير مقدمة من قبل حاوية EJB3. يتم إنشاء هذه المعاملات وإدارتها بواسطة Spring وتخضع للتكوينات في الملف [spring-config.xml].

spring-config.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">

    <!-- طبقات التطبيقات -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>

    <!-- طبقة الاستمرارية 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" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean
                class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- مصدر البيانات DBCP -->
    <bean id="dataSource"
        class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>

    <!-- مدير المعاملات -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory"
            ref="entityManagerFactory" />
    </bean>

    <!-- ترجمة الاستثناءات -->
    <bean
        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- تعليقات الاستمرارية -->
    <bean
        class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>
  • الأسطر 2-5: العلامة الجذرية <beans> لملف التكوين. لن نعلق على السمات المختلفة لهذه العلامة. يجب الحرص على النسخ واللصق لأن الخطأ في إحدى هذه السمات يؤدي إلى أخطاء يصعب فهمها أحيانًا.
  • السطر 8: bean "dao" هو مرجع إلى مثيل من فئة [dao.Dao]. سيتم إنشاء مثيل واحد (singleton) وسيقوم بتنفيذ طبقة [dao] من التطبيق.
  • الأسطر 9-11: إنشاء مثيل للطبقة [service]. البيان "service" هو مرجع إلى مثيل من فئة [service.Service]. سيتم إنشاء مثيل واحد (singleton) وسيقوم بتنفيذ طبقة [service] للتطبيق. لقد رأينا أن الفئة [service.Service] تحتوي على حقل خاص [IDao dao]. يتم تهيئة هذا الحقل في السطر 10 بواسطة bean "dao" المحدد في السطر 8.
  • في النهاية، قامت الأسطر 8-11 بتكوين الطبقتين [dao] و [service]. سنرى لاحقًا متى وكيف سيتم إنشاء مثيلات لهما.
  • الأسطر 35-42: يتم تعريف مصدر بيانات. لقد تعرفنا بالفعل على مفهوم مصدر البيانات عند دراسة الكيانات JPA مع Hibernate:

فيما سبق، كان من الممكن تسمية [c3p0] المسمى "مجمع الاتصالات" بـ"مصدر البيانات". يوفر مصدر البيانات خدمة "مجمع الاتصالات". مع Spring، سنستخدم مصدر بيانات بخلاف [c3p0]. وهو [DBCP] من مشروع Apache commons DBCP [http://jakarta.apache.org/commons/dbcp/]. تم وضع أرشيفات [DBCP] في مكتبة المستخدم [jpa-spring]:

 
  • الأسطر 38-41: لإنشاء اتصالات مع قاعدة البيانات المستهدفة، تحتاج مصدر البيانات إلى معرفة برنامج التشغيل Jdbc المستخدم (السطر 38)، وعنوان URL لقاعدة البيانات (السطر 39)، ومستخدم الاتصال وكلمة مروره (الأسطر 40-41).
  • الأسطر 14-32: تكوين الطبقة JPA
  • السطران 14-15: يحددان حبة (bean) من النوع [EntityManagerFactory] قادرة على إنشاء كائنات من النوع [EntityManager] لإدارة سياقات الاستمرارية. يتم توفير الفئة التي تم إنشاء مثيل لها [LocalContainerEntityManagerFactoryBean] بواسطة Spring. وهي تحتاج إلى عدد معين من المعلمات لإنشاء مثيل لها، والتي تم تعريفها في الأسطر 16-31.
  • السطر 16: مصدر البيانات الذي سيتم استخدامه للحصول على اتصالات بـ SGBD. وهو مصدر [DBCP] المحدد في الأسطر 35-42.
  • الأسطر 17-27: التنفيذ JPA المطلوب استخدامه
  • الأسطر 18-26: تحدد Hibernate (السطر 19) كالتنفيذ JPA المطلوب استخدامه
  • الأسطر 23-24: اللهجة SQL التي يجب أن يستخدمها Hibernate مع الهدف SGBD، وهو هنا MySQL5.
  • السطر 25: يطلب أن يتم إنشاء قاعدة البيانات (drop و create) عند بدء تشغيل التطبيق.
  • الأسطر 28-31: تحدد "محمل الفئات". لا أستطيع شرح دور هذا البين الذي يستخدمه EntityManagerFactory في الطبقة JPA بشكل واضح. ومع ذلك، فإنه يتطلب تمرير اسم أرشيف إلى JVM الذي يقوم بتشغيل التطبيق، حيث سيتولى محتوى هذا الأرشيف إدارة تحميل الفئات عند بدء تشغيل التطبيق. هنا، هذا الأرشيف هو [spring-agent.jar] الموجود في مكتبة المستخدم [jpa-spring] (انظر أعلاه). سنرى أن Hibernate لا يحتاج إلى هذا الوكيل ولكن Toplink يحتاج إليه.
  • الأسطر 45-50: تحدد مدير المعاملات الذي سيتم استخدامه
  • السطر 45: يشير إلى أن المعاملات تُدار باستخدام تعليقات توضيحية Java (كان من الممكن أيضًا الإعلان عنها في spring-config.xml). ويتعلق الأمر على وجه الخصوص بالتعليق التوضيحي @Transactional الموجود في الفئة [Service] (السطر 6).
  • الأسطر 46-50: مدير المعاملات
  • السطر 47: مدير المعاملات هو فئة مقدمة من Spring
  • الأسطر 48-49: يحتاج مدير المعاملات في Spring إلى معرفة EntityManagerFactory الذي يدير الطبقة JPA. وهو المحدد في الأسطر 14-32.
  • السطران 57-58: يحددان الفئة التي تدير تعليقات Spring الخاصة بالاستمرارية الموجودة في كود Java، مثل التعليق @PersistenceContext للفئة [dao.Dao] (السطر 12).
  • السطور 53-54: تحدد فئة Spring التي تدير بشكل خاص التعليق التوضيحي @Repository الذي يجعل الفئة المُعلَّمة بهذه الطريقة مؤهلة لترجمة الاستثناءات الأصلية لمحرك Jdbc من SGBD إلى استثناءات Spring عامة من النوع [DataAccessException]. يغلف هذا التحويل الاستثناء الأصلي لـ Jdbc في نوع [DataAccessException] الذي يحتوي على فئات فرعية متنوعة:

Image

تسمح هذه الترجمة لبرنامج العميل بإدارة الاستثناءات بشكل عام بغض النظر عن الهدف SGBD. لم نستخدم التعليق التوضيحي @Repository في كود Java الخاص بنا. لذا فإن السطور 53-54 غير ضرورية. وقد تركناها لمجرد توضيح المعلومات.

لقد انتهينا من ملف تكوين Spring. إنه معقد ولا تزال هناك الكثير من الأمور الغامضة. وقد تم استخراجه من وثائق Spring. لحسن الحظ، غالبًا ما يقتصر تكييفه مع المواقف المختلفة على تعديلين:

  • تعديل قاعدة البيانات المستهدفة: الأسطر 38-41. سنقدم مثالاً على Oracle.
  • تعديل التنفيذ JPA: الأسطر 14-32. سنقدم مثالاً على Toplink.

3.1.6. برنامج العميل [InitDB]

نتناول كتابة أول عميل للبنية الموصوفة سابقًا:

رمز [InitDB] هو التالي:


package tests;

...
public class InitDB {

    // طبقة الخدمة
    private static IService service;

    // المنشئ
    public static void main(String[] args) throws ParseException {
        // تكوين التطبيق
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // طبقة الخدمة
        service = (IService) ctx.getBean("service");
        // إفراغ قاعدة البيانات
        clean();
        // ملء القاعدة
        fill();
        // التحقق بصريًا
        dumpPersonnes();
    }

    // عرض محتوى الجدول
    private static void dumpPersonnes() {
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // ملء الجدول
    public static void fill() throws ParseException {
        // إنشاء أشخاص
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // يتم حفظها
        service.saveArray(new Personne[] { p1, p2 });
    }

    // حذف عناصر من الجدول
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • السطر 12: يتم استخدام الملف [spring-config.xml] لإنشاء كائن [ApplicationContext ctx] الذي يمثل صورة ذاكرة للملف. يتم إنشاء مثيلات للـ beans المحددة في [spring-config.xml] في هذه المرحلة.
  • السطر 14: يُطلب من سياق التطبيق ctx مرجعًا على الطبقة [service]. ومن المعروف أن هذه الطبقة ممثلة بواسطة عنصر يُسمى "service".
  • السطر 16: يتم إفراغ قاعدة البيانات باستخدام الطريقة clean في الأسطر 41-45:
    • الأسطر 42-44: نطلب قائمة بجميع الأشخاص من سياق الاستمرارية ونقوم بتكرارها لحذفها واحدًا تلو الآخر. ربما نتذكر أن [spring-config.xml] تحدد أن قاعدة البيانات يجب أن يتم إنشاؤها عند بدء تشغيل التطبيق. لذلك، في حالتنا هذه، لا فائدة من استدعاء الطريقة clean لأننا نبدأ بقاعدة بيانات فارغة.
  • السطر 18: تملأ الطريقة fill قاعدة البيانات. يتم تعريف هذه القاعدة في الأسطر 32-38:
    • السطران 34-35: يتم إنشاء شخصين
    • السطر 37: يُطلب من الطبقة [service] جعلهما دائمين.
  • السطر 20: تعرض الطريقة dumpPersonnes الأشخاص الدائمين. وهي محددة في الأسطر 24-29
    • الأسطر 26-28: يُطلب من الطبقة [service] قائمة بجميع الأشخاص الدائمين ويتم عرضهم على وحدة التحكم.

يؤدي تنفيذ [InitDB] إلى النتيجة التالية:

1
2
3
[personnes]
[72,0,p1,Paul,31/01/2000,true,2]
[73,0,p2,Sylvie,05/07/2001,false,0]

3.1.7. اختبارات وحدة [TestNG]

يتم وصف تثبيت المكون الإضافي [TestNG] في الفقرة 5.2.4. رمز برنامج [TestNG] هو كما يلي:


package tests;

....
public class TestNG {

    // طبقة الخدمة
    private IService service;

    @BeforeClass
    public void init() {
        // السجل
        log("init");
        // تكوين التطبيق
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // طبقة الخدمة
        service = (IService) ctx.getBean("service");
    }

    @BeforeMethod
    public void setUp() throws ParseException {
        // إفراغ قاعدة البيانات
        clean();
        // ملء قاعدة البيانات
        fill();
    }

    // السجلات
    private void log(String message) {
        System.out.println("----------- " + message);
    }

    // عرض محتوى الجدول
    private void dump() {
        log("dump");
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // ملء الجدول
    public void fill() throws ParseException {
        log("fill");
        // إنشاء أشخاص
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // التي يتم حفظها
        service.saveArray(new Personne[] { p1, p2 });
    }

    // حذف عناصر من الجدول
    public void clean() {
        log("clean");
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }

    @Test()
    public void test01() {
...
    }
...
}
  • السطر 9: تشير التعليقات التوضيحية @BeforeClass إلى الطريقة التي يجب تنفيذها لتهيئة الإعدادات اللازمة للاختبارات. يتم تنفيذها قبل تنفيذ الاختبار الأول. تشير التعليقات التوضيحية @AfterClass غير المستخدمة هنا إلى الطريقة التي يجب تنفيذها بمجرد تنفيذ جميع الاختبارات.
  • السطور 10-17: تستخدم الطريقة init المُعلَّمة بـ @BeforeClass ملف تكوين Spring لإنشاء مثيلات لمختلف طبقات التطبيق والحصول على مرجع للطبقة [service]. ثم تستخدم جميع الاختبارات هذا المرجع.
  • السطر 19: تشير العلامة @BeforeMethod إلى الطريقة التي يجب تنفيذها قبل كل اختبار. أما العلامة @AfterMethod، التي لم تُستخدم هنا، فتشير إلى الطريقة التي يجب تنفيذها بعد كل اختبار.
  • السطور 20-25: تقوم الطريقة setUp المُعلَّمة بـ @BeforeMethod بإفراغ قاعدة البيانات (clean الأسطر 52-56) ثم ملؤها بشخصين (fill الأسطر 42-49).
  • السطر 59: يشير التعليق التوضيحي @Test إلى طريقة اختبار يجب تنفيذها. سنقوم الآن بوصف هذه الاختبارات.

@Test()
    public void test01() {
        log("test1");
        dump();
        // قائمة الأشخاص
        List<Personne> personnes = service.getAll();
        assert 2 == personnes.size();
    }

    @Test()
    public void test02() {
        log("test2");
        // البحث عن الأشخاص حسب أسمائهم
        List<Personne> personnes = service.getAllLike("p1%");
        assert 1 == personnes.size();
        Personne p1 = personnes.get(0);
        assert "Paul".equals(p1.getPrenom());
    }

    @Test()
    public void test03() throws ParseException {
        log("test3");
        // إنشاء شخص جديد
        Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // حفظها
        service.saveOne(p3);
        // إعادة طلبها
        Personne loadedp3 = service.getOne(p3.getId());
        // عرضها
        System.out.println(loadedp3);
        // التحقق
        assert "p3".equals(loadedp3.getNom());
    }
  • الأسطر 2-8: الاختبار 01. يجب أن نتذكر أنه في بداية كل اختبار، تحتوي قاعدة البيانات على شخصين باسمي p1 و p2.
  • السطر 6: نطلب قائمة الأشخاص
  • السطر 7: نتحقق من أن عدد الأشخاص في القائمة التي تم الحصول عليها هو 2
  • السطر 14: نطلب قائمة بالأشخاص الذين يبدأ اسمهم بـ p1
  • نتحقق من أن القائمة التي تم الحصول عليها تحتوي على عنصر واحد فقط (السطر 15) وأن الاسم الأول للشخص الوحيد الذي تم الحصول عليه هو "Paul" (السطر 17)
  • السطر 24: يتم إنشاء شخص باسم p3
  • السطر 25: يتم حفظها
  • السطر 28: نطلبها مرة أخرى من سياق التخزين للتحقق
  • السطر 32: نتحقق من أن الشخص الذي تم الحصول عليه يحمل بالفعل الاسم p3.

@Test()
    public void test04() throws ParseException {
        log("test4");
        // تحميل الشخص p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // نعرضها
        System.out.println(p1);
        // التحقق
        assert "p1".equals(p1.getNom());
        int version1 = p1.getVersion();
        // يتم تعديل الاسم الأول
        p1.setPrenom("x");
        // يتم الحفظ
        service.updateOne(p1);
        // إعادة التحميل
        p1 = service.getOne(p1.getId());
        // عرض
        System.out.println(p1);
        // التحقق من زيادة رقم الإصدار
        assert (version1 + 1) == p1.getVersion();

    }
  • السطر 5: نطلب الشخص p1
  • السطر 10: نتحقق من اسمه
  • السطر 11: يتم تدوين رقم الإصدار الخاص به
  • السطر 13: يتم تعديل اسمه الأول
  • السطر 15: يتم حفظ التعديل
  • السطر 17: نطلب الشخص p1 مرة أخرى
  • السطر 21: نتحقق من أن رقم الإصدار قد زاد بمقدار 1

@Test()
    public void test05() {
        log("test5");
        // تحميل الشخص p2
        List<Personne> personnes = service.getAllLike("p2%");
        Personne p2 = personnes.get(0);
        // عرضه
        System.out.println(p2);
        // التحقق
        assert "p2".equals(p2.getNom());
        // نحذف الشخص p2
        service.deleteOne(p2.getId());
        // إعادة تحميله
        p2 = service.getOne(p2.getId());
        // نتحقق من الحصول على مؤشر null
        assert null == p2;
        // عرض الجدول
        dump();
    }
  • السطر 5: نطلب الشخص p2
  • السطر 10: التحقق من اسمها
  • السطر 12: يتم حذفها
  • السطر 14: نطلبه مرة أخرى
  • السطر 16: يتم التحقق من عدم العثور عليها

@Test()
    public void test06() throws ParseException {
        log("test6");
        // يتم إنشاء مصفوفة من شخصين يحملان نفس الاسم (مخالفة لقاعدة تفرد الاسم)
        Personne[] personnes = { new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2)};
        // نحفظ هذا المصفوف - يجب أن نحصل على استثناء وتراجع
        boolean erreur = false;
        try {
            service.saveArray(personnes);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // تفريغ
        dump();
        // التحقق
        assert erreur;
        // البحث عن شخص باسم p3
        List<Personne> personnesp3 = service.getAllLike("p3%");
        assert 0 == personnesp3.size();
        // تفريغ
        dump();
    }
  • السطر 5: ننشئ جدولًا يضم ثلاثة أشخاص، اثنان منهم يحملان نفس الاسم "p4". وهذا يخالف قاعدة تفرد اسم @Entity Personne:

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
  • السطر 11: يتم وضع المصفوفة المكونة من ثلاثة أشخاص في سياق الاستمرارية. ومن المفترض أن تفشل عملية إضافة الشخص الثاني p4. ونظرًا لأن الطريقة [saveArray] تتم في معاملة، فسيتم إلغاء جميع عمليات الإدراج التي تمت قبل ذلك. وفي النهاية، لن تتم أي عملية إضافة.
  • السطر 18: يتم التحقق من أن [saveArray] قد أطلقت استثناءً بالفعل
  • السطران 20-21: نتحقق من أن الشخص p3 الذي كان من الممكن إضافته لم تتم إضافته.

@Test()
    public void test07() {
        log("test7");
        // اختبار القفل المتفائل
        // تحميل الشخص p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // عرضه
        System.out.println(p1);
        // زيادة عدد أطفاله
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // نقوم بحفظ p1
        Personne newp1 = service.updateOne(p1);
        assert (nbEnfants1 + 1) == newp1.getNbenfants();
        System.out.println(newp1);
        // يتم الحفظ للمرة الثانية - يجب أن تحدث استثناء لأن p1 لم يعد لديه الإصدار الصحيح
        // إنه newp1 الذي يمتلكه
        boolean erreur = false;
        try {
            service.updateOne(p1);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // التحقق
        assert erreur;
        // يتم زيادة عدد أبناء newp1
        int nbEnfants2 = newp1.getNbenfants();
        newp1.setNbenfants(nbEnfants2 + 1);
        // نقوم بحفظ newp1
        service.updateOne(newp1);
        // إعادة التحميل
        p1 = service.getOne(p1.getId());
        // التحقق
        assert (nbEnfants1 + 2) == p1.getNbenfants();
        System.out.println(p1);
    }
  • السطر 6: نطلب الشخص p1
  • السطر 12: زيادة عدد أطفاله بمقدار 1
  • السطر 14: يتم تحديث الشخص p1 في سياق الاستمرارية. تعمل الطريقة [updateOne] على جعل الإصدار الجديد newp1 مستمرًا من p1. ويختلف عن p1 في رقم الإصدار الذي كان يجب زيادته.
  • السطر 15: يتم التحقق من عدد أطفال newp1.
  • السطر 21: نطلب تحديثًا جديدًا للشخص p1 من الإصدار القديم p1. يجب أن تحدث استثناء لأن p1 ليس الإصدار الأخير للشخص p1. الإصدار الأخير هو newp1.
  • السطر 23: نتحقق من حدوث الخطأ بالفعل
  • الأسطر 27-35: نتحقق من أنه إذا تم إجراء تحديث من أحدث إصدار newp1، فإن الأمور تسير على ما يرام.

@Test()
    public void test08() {
        log("test8");
        // اختبار التراجع على updateArray
        // تحميل الشخص p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // عرض
        System.out.println(p1);
        // زيادة عدد أطفاله
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // يتم حفظ تعديلين، يجب أن يفشل الثاني (الشخص غير مهيأ بشكل صحيح)
        // بسبب المعاملة، يجب إلغاء كلا التعديلين
        boolean erreur = false;
        try {
            service.updateArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // التحقق
        assert erreur;
        // يتم إعادة تحميل الشخص p1
        personnes = service.getAllLike("p1%");
        p1 = personnes.get(0);
        // لم يتغير عدد أطفاله
        assert nbEnfants1 == p1.getNbenfants();
    }
  • الاختبار 8 مشابه للاختبار 6: فهو يتحقق من صحة rollback عند معالجته بواسطة updateArray على مصفوفة مكونة من شخصين، حيث لم يتم تهيئة الشخص الثاني بشكل صحيح. من وجهة نظر JPA، فإن عملية الدمج على الشخص الثاني الذيموجودة من قبل ستولد أمر SQL insert الذي سيفشل بسبب القيود nullable=false الموجودة على بعض حقول الكيان Personne.

@Test()
    public void test09() {
        log("test9");
        // اختبار التراجع على deleteArray
        // تفريغ
        dump();
        // يتم تحميل الشخص p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // نقوم بعرضه
        System.out.println(p1);
        // يتم إجراء عمليتي حذف، يجب أن تفشل الثانية (شخص غير معروف)
        // بسبب المعاملة، يجب إلغاء كلاهما
        boolean erreur = false;
        try {
            service.deleteArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // التحقق
        assert erreur;
        // يتم إعادة تحميل الشخص p1
        personnes = service.getAllLike("p1%");
        // التحقق
        assert 1 == personnes.size();
        // تفريغ
        dump();
    }
  • الاختبار 9 مشابه للاختبار السابق: فهو يتحقق من rollback على deleteArray الذي يعمل على جدول مكون من شخصين حيث لا يوجد الشخص الثاني. ولكن في هذه الحالة، تطلق الطريقة [deleteOne] من الطبقة [dao] استثناءً.

// قفل متفائل - وصول متعدد الخيوط
    @Test()
    public void test10() throws Exception {
        // إضافة شخص
        Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p3);
        int id3 = p3.getId();
        // إنشاء N خيوط لتحديث عدد الأطفال
        final int N = 20;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajEnfants("thread n° " + i, service, id3);
            taches[i].start();
        }
        // في انتظار انتهاء الخيوط
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // استرداد الشخص
        p3 = service.getOne(id3);
        // يجب أن يكون لديها N أطفال
        assert N == p3.getNbenfants();
        // حذف الشخص p3
        service.deleteOne(p3.getId());
        // التحقق
        p3 = service.getOne(p3.getId());
        // يجب أن يكون هناك مؤشر null
        assert p3 == null;
    }
  • تتمثل فكرة الاختبار 10 في تشغيل N خيوط (السطر 9) لزيادة عدد أبناء الشخص بشكل متوازٍ. نريد التحقق من أن نظام رقم الإصدار يتحمل هذه الحالة بشكل جيد. فقد تم إنشاؤه لهذا الغرض.
  • السطران 5-6: يتم إنشاء شخص باسم p3 ثم يتم حفظه. لديه 0 طفل في البداية.
  • السطر 7: يتم تسجيل معرفها.
  • السطور 9-14: يتم تشغيل N خيوط بشكل متوازٍ، وكلها مكلفة بزيادة عدد أطفال p3 بمقدار 1.
  • الأسطر 16-18: ننتظر انتهاء جميع الخيوط
  • السطر 20: نطلب عرض الشخص p3
  • السطر 22: نتحقق من أن لديها الآن N أطفال
  • السطر 24: يتم حذف الشخص p3.

الخيط [ThreadMajEnfants] هو التالي:


package tests;

...
public class ThreadMajEnfants extends Thread {
    // اسم الخيط
    private String name;

    // مرجع على الطبقة [service]
    private IService service;

    // معرف الشخص الذي سنعمل عليه
    private int idPersonne;

    // المنشئ
    public ThreadMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    // جوهر الموضوع
    public void run() {
        // المتابعة
        suivi("lancé");
        // نكرر العملية حتى ننجح في زيادة القيمة بمقدار 1
        // عدد أبناء الشخص idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // يتم استرداد نسخة من الشخص من idPersonne
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // متابعة
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // يتم زيادة عدد أطفال الشخص بمقدار 1
            personne.setNbenfants(nbEnfants + 1);
            // انتظار لمدة 10 مللي ثانية قبل التخلي عن المعالج
            try {
                // متابعة
                suivi("début attente");
                // التوقف لإفساح المجال للمعالج
                Thread.sleep(10);
                // متابعة
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // انتهى الانتظار - محاولة التحقق من صحة النسخة
            // في غضون ذلك، تمكنت خيوط أخرى من تعديل الأصل
            try {
                // نحاول تعديل الأصل
                service.updateOne(personne);
                // تمت العملية - تم تعديل الأصل
                fini = true;
            } catch (javax.persistence.OptimisticLockException e) {
                // إصدار الكائن غير صحيح: يتم تجاهل الاستثناء لإعادة المحاولة
            } catch (org.springframework.transaction.UnexpectedRollbackException e2) {
                // استثناء Spring يظهر من حين لآخر
            } catch (RuntimeException e3) {
                // نوع آخر من الاستثناءات - يتم تمريره
                throw e3;
            }
        }
        // متابعة
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

    // متابعة
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : " + message);
    }
}
  • الأسطر 15-19: يقوم المنشئ بتخزين المعلومات التي يحتاجها للعمل: اسمه (السطر 16)، المرجع على الطبقة [service] الذي يجب أن يستخدمه (السطر 17) ومعرف الشخص p الذي يجب أن يزيد عدد أطفاله (السطر 18).
  • الأسطر 22-66: الطريقة [run] التي يتم تنفيذها بواسطة جميع الخيوط بشكل متوازٍ.
  • السطر 29: يحاول الخيط بشكل متكرر زيادة عدد أطفال الشخص p. ولا يتوقف إلا عندما ينجح في ذلك.
  • السطر 31: يتم طلب الشخص p
  • السطر 36: يتم زيادة عدد أطفاله في الذاكرة
  • الأسطر 38-47: يتم إيقاف مؤقت لمدة 10 مللي ثانية. سيسمح ذلك لخيوط أخرى بالحصول على نفس نسخة الشخص p. وبالتالي، سيكون لدينا في نفس الوقت عدة خيوط تمتلك نفس نسخة الشخص p وترغب في تعديلها. وهذا هو المطلوب.
  • السطر 52: بمجرد انتهاء فترة التوقف، يطلب الخيط من الطبقة [service] الاحتفاظ بالتعديل. ونحن نعلم أنه ستكون هناك استثناءات من حين لآخر، لذا قمنا بتأطير العملية بعبارة try / catch.
  • السطر 55: تُظهر الاختبارات وجود استثناءات من النوع [javax.persistence.OptimisticLockException]. هذا أمر طبيعي: إنه الاستثناء الذي تطلقه الطبقة JPA عندما يحاول مؤشر ترابط تعديل الشخص p دون أن يكون لديه أحدث إصدار منه. يتم تجاهل هذا الاستثناء للسماح لمؤشر الترابط بمحاولة العملية مرة أخرى حتى ينجح.
  • السطر 57: تظهر الاختبارات أننا نواجه أيضًا استثناءات من النوع [org.springframework.transaction.UnexpectedRollbackException]. هذا أمر مزعج وغير متوقع. ليس لدي أي تفسير لذلك. ها نحن نعتمد على Spring بينما كنا نرغب في تجنب ذلك. هذا يعني أنه إذا قمنا بتشغيل تطبيقنا في JBoss Ejb3 على سبيل المثال، فسيتعين تغيير كود الخيط. يتم تجاهل استثناء Spring هنا أيضًا للسماح للخيط بمحاولة إجراء عملية الزيادة مرة أخرى.
  • السطر 59: يتم رفع أنواع الاستثناءات الأخرى إلى التطبيق.

عند تنفيذ [TestNG]، نحصل على النتائج التالية:

Image

تم اجتياز الاختبارات العشرة بنجاح.

يستحق الاختبار رقم 10 توضيحات إضافية لأن نجاحه ينطوي على جانب سحري. لنعد أولاً إلى تكوين الطبقة [dao]:


public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

  • السطر 4: يتم إدخال كائن [EntityManager] في الحقل em بفضل التعليق التوضيحي JPA @PersistenceContext. يتم إنشاء مثيل لطبقة [dao] مرة واحدة فقط. وهي عبارة عن عنصر فريد يستخدمه جميع الخيوط التي تستخدم الطبقة JPA. وبالتالي، فإن EntityManager em مشترك بين جميع الخيوط. يمكن التحقق من ذلك عن طريق عرض قيمة em في الطريقة [updateOne] المستخدمة من قبل الخيوط [ThreadMajEnfants]: نحصل على نفس القيمة لجميع الخيوط.

وبالتالي، يمكننا أن نتساءل عما إذا كانت الكائنات الدائمة للخيوط المختلفة التي يتم التعامل معها بواسطة em في EntityManager، وهو نفسه لجميع الخيوط، لن تختلط وتخلق تعارضات فيما بينها. يوجد مثال على ما قد يحدث في [ThreadMajEnfants]:


        while (!fini) {
            // يتم استرداد نسخة من الشخص من idPersonne
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // متابعة
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // زيادة عدد أبناء الشخص بمقدار 1
            personne.setNbenfants(nbEnfants + 1);
            // الانتظار لمدة 10 مللي ثانية لإخلاء المعالج
            try {
                // متابعة
                suivi("début attente");
                // التوقف لإفساح المجال للمعالج
                Thread.sleep(10);
                // متابعة
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
}
  • السطر 3: يسترجع مؤشر ترابط T1 الشخص p
  • السطر 8: يزيد عدد أطفال p
  • السطر 14: يقوم الخيط T1 بالتوقف مؤقتًا

يستحوذ مؤشر الترابط T2 على السيطرة ويقوم هو الآخر بتنفيذ السطر 3: فهو يطلب نفس الشخص p الذي طلبه T1. إذا كان سياق استمرارية الخيوط هو نفسه، فإن الشخص p الموجود بالفعل في السياق بفضل T1 يجب أن يُعاد إلى T2. في الواقع، تستخدم الطريقة [getOne] الطريقة [EntityManager].تبحث API عن JPA، ولا تقوم هذه الطريقة بالوصول إلى قاعدة البيانات إلا إذا كان الكائن المطلوب غير موجود في سياق الاستمرارية، وإلا فإنها تعرض الكائن الموجود في سياق الاستمرارية. إذا كان الأمر كذلك، فإن T1 و T2 سيحتفظان بنفس الشخص p. عندئذٍ ستزيد T2 عدد أبناء p بمقدار 1 مرة أخرى (السطر 8). إذا نجح أحد الخيوط في التحديث بعد التوقف المؤقت، فسيكون عدد أبناء p قد زاد بمقدار 2 وليس 1 كما هو متوقع. عندئذٍ يمكن أن نتوقع أن تمرر الخيوط N عدد الأبناء ليس إلى N بل إلى أكثر من ذلك. لكن هذا ليس هو الحال. يمكننا إذن أن نستنتج أن T1 و T2 لا يشتركان في نفس المرجع p. نتحقق من ذلك عن طريق عرض عنوان p بواسطة الخيوط: فهو مختلف لكل منها.

لذلك يبدو أن الخيوط:

  • تتشارك نفس مدير سياق الاستمرارية (EntityManager)
  • ولكن لكل منها سياق استمرارية خاص بها.

هذه مجرد افتراضات، وسيكون من المفيد هنا الحصول على رأي خبير.

3.1.8. تغيير SGBD

لتغيير SGBD، ما عليك سوى استبدال الملف [src/spring-config.xml] [2] بالملف [spring-config.xml] الخاص بـ SGBD المعني من المجلد [conf] [1].

ملف [spring-config.xml] من Oracle هو، على سبيل المثال، التالي:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

...
    <bean id="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.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- مصدر البيانات DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.OracleDriver" />
        <property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
...
</beans>

تتغير بعض الأسطر فقط مقارنةً بالملف نفسه المستخدم سابقًا لـ MySQL5:

  • السطر 14: اللهجة SQL التي يجب أن يستخدمها Hibernate
  • الأسطر 25-28: خصائص اتصال Jdbc مع SGBD

يُطلب من القارئ تكرار الاختبارات الموصوفة لـ MySQL5 مع SGBD أخرى.

3.1.9. تغيير تنفيذ JPA

لنعد إلى بنية الاختبارات السابقة:

نستبدل تطبيق JPA / Hibernate بتطبيق JPA / Toplink. نظرًا لأن Toplink لا يستخدم نفس المكتبات التي يستخدمها Hibernate، فإننا نستخدم مشروع Eclipse جديدًا:

  • في [1]: مشروع Eclipse. وهو مطابق للمشروع السابق. الشيء الوحيد الذي تغير هو ملف التكوين [spring-config.xml] [2] والمكتبة [jpa-toplink] التي تحل محل المكتبة [jpa-hibernate].
  • في [3]: مجلد أمثلة هذا البرنامج التعليمي. في [4] مشروع Eclipse المراد استيراده.

يصبح ملف التكوين [spring-config.xml] لـ Toplink كما يلي:


<?xml version="1.0" encoding="UTF-8"?>

<!-- يجب تشغيل JVM مع الوسيطة -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à remplacer par le chemin exact de spring-agent.jar)-->

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

    <!-- طبقات التطبيق -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- مصدر البيانات DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>

    <!-- مدير المعاملات -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>

    <!-- ترجمة الاستثناءات -->
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- الاستمرارية -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>

يجب تغيير بضعة أسطر للانتقال من Hibernate إلى Toplink:

  • السطر 19: يتم الآن تنفيذ JPA بواسطة Toplink
  • السطر 23: الخاصية [databasePlatform] لها قيمة مختلفة عن تلك الموجودة في Hibernate: اسم فئة خاصة بـ Toplink. تم شرح مكان العثور على هذا الاسم في الفقرة 2.1.15.2.

هذا كل شيء. تجدر الإشارة إلى السهولة التي يمكن بها تغيير SGBD أو التنفيذ JPA باستخدام Spring.

لكننا لم ننتهي تمامًا بعد. عند تشغيل [InitDB] على سبيل المثال، نحصل على استثناء ليس من السهل فهمه:


Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [spring-config.xml]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.
Caused by: java.lang.IllegalStateException: Must start with Java agent to use 

تحثنا رسالة الخطأ في السطر 1 على قراءة وثائق Spring. وهناك نكتشف المزيد عن الدور الذي تلعبه إعلان غامض في الملف [spring-config.xml]:


    <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.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
</bean>

تشير السطر 1 من الاستثناء إلى فئة تسمى [InstrumentationLoadTimeWeaver]، وهي فئة نجدها في السطر 13 من ملف تكوين Spring. توضح وثائق Spring أن هذه الفئة ضرورية في بعض الحالات لتحميل فئات التطبيق، وأنه لكي يتم استخدامها، يجب تشغيل JVM مع وكيل. يتم توفير هذا الوكيل بواسطة Spring ويسمى [spring-agent]:

  • يوجد الملف [spring-agent.jar] في المجلد <exemples>/lib [1]. يتم توفيره مع توزيع Spring 2.x (انظر الفقرة 5.11).
  • في [3]، يتم إنشاء تكوين تشغيل [Run/Run...]
  • في [4]، نقوم بإنشاء تكوين تشغيل Java (هناك أنواع مختلفة من تكوينات التشغيل)
  • في [5]، نختار علامة التبويب [Main]
  • في [6]، نسمي التكوين
  • في [7]، قم بتسمية مشروع Eclipse المعني بهذا الإعداد (استخدم الزر Browse)
  • في [8]، قم بتسمية فئة Java التي تحتوي على الطريقة [main] (استخدم زر Browse)
  • في [9]، ننتقل إلى علامة التبويب [Arguments]. في هذه العلامة، يمكن تحديد نوعين من الحجج:
    • في [9]، تلك التي تم تمريرها إلى الطريقة [main]
    • في [10]، تلك التي تم تمريرها إلى JVM التي ستقوم بتنفيذ الكود. يتم تعريف وكيل Spring باستخدام المعلمة -javaagent:قيمة في JVM. القيمة هي مسار الملف [spring-agent.jar].
  • في [11]: يتم التحقق من صحة التكوين
  • في [12]: يتم إنشاء التكوين
  • في [13]: يتم تنفيذه

بعد ذلك، يتم تنفيذ [InitDB] ويعطي نفس النتائج التي يعطيها Hibernate. بالنسبة لـ [TestNG]، يجب اتباع نفس الطريقة:

  • في [1]، نقوم بإنشاء تكوين للتنفيذ [Run/Run...]
  • في [2]، يتم إنشاء تكوين تشغيل TestNG
  • في [3]، نختار علامة التبويب [Test]
  • في [4]، نسمي التكوين
  • في [5]، نسمي مشروع Eclipse المعني بهذا التكوين (استخدم زر Browse)
  • في [6]، قم بتسمية فئة الاختبارات (استخدم الزر Browse)
  • في [7]، ننتقل إلى علامة التبويب [Arguments].
  • في [8]: يتم تعيين الحجة -javaagent لـ JVM.
  • في [9]: يتم التحقق من صحة التكوين
  • في [10]: يتم إنشاء التكوين
  • في [11]: يتم تنفيذه

بعد ذلك، يتم تنفيذ [TestNG] ويعطي نفس النتائج التي يعطيها Hibernate.

3.2. مثال 2: JBoss EJB3 / JPA مع كيان Personne

نستخدم نفس المثال السابق، لكننا ننفذه في حاوية EJB3، وهي حاوية JBoss:

عادةً ما يتم دمج حاوية Ejb3 في خادم تطبيقات. توفر JBoss حاوية Ejb3 "مستقلة" يمكن استخدامها خارج خادم التطبيقات. سنكتشف أنها توفر خدمات مشابهة لتلك التي يوفرها Spring. سنحاول معرفة أي من هذه الحاويات هو الأكثر فائدة.

يتم وصف تثبيت حاوية JBoss Ejb3 في الفقرة 5.12.

3.2.1. مشروع Eclipse / Jboss Ejb3 / Hibernate

مشروع Eclipse هو التالي:

  • في [1]: مشروع Eclipse. يمكن العثور عليه في [6] ضمن أمثلة البرنامج التعليمي [5]. سنقوم باستيراده.
  • في [2]: أكواد Java للطبقات المعروضة في الحزم:
    • [entites]: حزمة الكيانات JPA
    • [dao]: طبقة الوصول إلى البيانات - تعتمد على الطبقة JPA
    • [service]: طبقة خدمات أكثر منها طبقة مهنية. سيتم فيها استخدام خدمة المعاملات الخاصة بالحاوية Ejb3.
    • [tests]: تضم برامج الاختبار.
  • في [3]: تجمع مكتبة [jpa-jbossejb3] ملفات jar الضرورية لـ Jboss Ejb3 (انظر أيضًا [7] و [8]).
  • في [4]: المجلد [conf] يجمع ملفات التكوين لكل ملف من ملفات SGBD المستخدمة في هذا البرنامج التعليمي. يوجد ملفان في كل مرة: [persistence.xml] الذي يهيئ الطبقة JPA و [jboss-config.xml] الذي يهيئ الحاوية Ejb3.

3.2.2. الكيانات JPA

لا يوجد سوى كيان واحد مُدار هنا، وهو الكيان Personne الذي تمت دراسته سابقًا في الفقرة 3.1.2.

3.2.3. الطبقة [dao]

تقدم الطبقة [dao] الواجهة [IDao] الموصوفة سابقًا في الفقرة 3.1.3.

التنفيذ [Dao] لهذه الواجهة هو كما يلي:


package dao;

...
@Stateless
public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

    // حذف شخص عبر معرّفه
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }

    // الحصول على جميع الأشخاص
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    // الحصول على الأشخاص الذين يتطابق اسمهم مع نموذج
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // الحصول على شخص عبر معرّفه
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // حفظ شخص
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // تحديث شخص
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }

}
  • هذا الكود مطابق تمامًا للكود الذي استخدمناه مع Spring. فقط تعليقات Java هي التي تغيرت، وهذا ما سنناقشه.
  • السطر 4: تجعل التعليقة التوضيحية @Stateless من الفئة [Dao] كائن EJB بدون حالة. تجعل التعليقة التوضيحية @Stateful من الفئة كائن EJB مع حالة. يحتوي EJB ذو الحالة على حقول خاصة يجب الحفاظ على قيمتها بمرور الوقت. ومن الأمثلة الكلاسيكية على ذلك فئة تحتوي على معلومات مرتبطة بمستخدم الويب لتطبيق ما. ترتبط مثيل هذه الفئة بمستخدم محدد، وعندما ينتهي مؤشر ترابط تنفيذ طلب هذا المستخدم، يجب الاحتفاظ بالمثيل ليكون متاحًا عند الطلب التالي من نفس العميل. لا يحتوي Ejb @Stateless على حالة. إذا استخدمنا نفس المثال، عند انتهاء مؤشر ترابط تنفيذ طلب مستخدم، سينضم Ejb @Stateless إلى مجموعة من Ejb @Stateless ويصبح متاحًا لمؤشر ترابط تنفيذ طلب مستخدم آخر.
  • بالنسبة للمطور، فإن مفهوم Ejb3 @Stateless قريب من مفهوم singleton في Spring. وسيستخدمه في الحالات نفسها.
  • السطر 7: التعليق التوضيحي @PersistenceContext هو نفسه الموجود في إصدار Spring للطبقة [dao]. وهي تشير إلى الحقل الذي سيستقبل EntityManager الذي سيسمح للطبقة [dao] بمعالجة سياق الاستمرارية.
  • السطر 11: تُستخدم التعليقة التوضيحية @TransactionAttribute المطبقة على إحدى الطرق لتكوين المعاملة التي ستُنفذ فيها الطريقة. فيما يلي بعض القيم المحتملة لهذه التعليقة التوضيحية:
    • TransactionAttributeType.REQUIRED: يجب أن يتم تنفيذ الطريقة في معاملة. إذا كانت هناك معاملة قد بدأت بالفعل، فإن عمليات الاستمرارية الخاصة بالطريقة تتم ضمنها. وإلا، يتم إنشاء معاملة وبدءها.
    • TransactionAttributeType.REQUIRES_NEW: يجب أن يتم تنفيذ الطريقة في معاملة جديدة. يتم إنشاء هذه المعاملة وبدءها.
    • TransactionAttributeType.MANDATORY: يجب أن يتم تنفيذ هذه الطريقة ضمن معاملة قائمة. إذا لم تكن هناك معاملة قائمة، يتم إلقاء استثناء.
    • TransactionAttributeType.NEVER: لا يتم تنفيذ الطريقة أبدًا في معاملة.
    • ...

كان من الممكن وضع التعليق التوضيحي على الفئة نفسها:


@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Dao implements IDao {

يتم عندئذٍ تطبيق السمة على جميع طرق الفئة.

3.2.4. الطبقة [metier / service]

تقدم الطبقة [service] الواجهة [IService] التي تمت دراستها سابقًا في الفقرة 3.1.4. التنفيذ [Service] للواجهة [IService] مطابق للتنفيذ الذي تمت دراسته سابقًا في الفقرة 3.1.4، باستثناء ثلاثة تفاصيل:



@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {

    // طبقة [dao]
    @EJB
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

  • السطر 2: الفئة [Service] هي EJB بدون حالة
  • السطر 3: يجب أن تتم جميع أساليب الفئة [Service] في معاملة
  • السطران 7-8: سيتم حقن مرجع إلى EJB من الطبقة [dao] بواسطة حاوية EJB في الحقل [IDao dao] في السطر 8. التعليق التوضيحي @EJB في السطر 7 هو الذي يطلب هذا الإدخال. يجب أن يكون الكائن المدخل كائن Ejb. وهذا يمثل اختلافًا مهمًا عن Spring حيث يمكن إدخال أي نوع من الكائنات في كائن آخر.

3.2.5. تكوين الطبقات

يتم تكوين الطبقات [service] و [dao] و [JPA] بواسطة الملفات التالية:

  • يقوم [META-INF/persistence.xml] بتكوين الطبقة JPA
  • يقوم [jboss-config.xml] بتكوين الحاوية Ejb3. ويستخدم هو نفسه الملفات [default.persistence.properties, ejb3-interceptors-aop.xml, embedded-jboss-beans.xml, jndi.properties]. يتم توفير هذه الملفات مع Jboss Ejb3 وتضمن تكوينًا افتراضيًا لا يتم تغييره عادةً. لا يهتم المطور سوى بالملف [jboss-config.xml]

دعونا نلقي نظرة على ملفَي التكوين:

persistence.xml


<persistence 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_1_0.xsd" version="1.0">

    <persistence-unit name="jpa">

        <!-- المزود JPA هو Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- تدير بيئة Java DataSource JTA -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- البحث عن كيانات الطبقة JBA -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- سجلات SQL Hibernate
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- نوع SGBD المدار -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />

            <!-- إعادة إنشاء جميع الجداول (drop+create) عند نشر وحدة الاستمرارية -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

يشبه هذا الملف تلك التي سبق أن صادفناها في دراسة كيانات JPA. وهو يقوم بتكوين طبقة Hibernate JPA. فيما يلي المستجدات:

  • السطر 5: وحدة الاستمرارية jpa لا تحتوي على السمة transaction-type التي كانت موجودة دائمًا حتى الآن:

<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />

في حالة عدم وجود قيمة، يكون للسمة transaction-type القيمة الافتراضية "JTA" (لـ Java Transaction Api) التي تشير إلى أن مدير المعاملات يتم توفيره بواسطة حاوية Ejb3. يمكن لمدير "JTA" القيام بأكثر مما يقوم به مدير "RESOURCE_LOCAL": يمكنه إدارة المعاملات التي تغطي عدة اتصالات. باستخدام JTA، يمكن فتح معاملة t1 على اتصال c1 على SGBD 1، معاملة t2 على اتصال c2 مع SGBD 2، ويمكن اعتبار (t1,t2) كمعاملة واحدة إما تنجح فيها جميع العمليات (commit) أو لا تنجح أي منها (rollback).

هنا، نعمل مع مدير JTA لحاوية Jboss Ejb3.

  • السطر 11: يعلن عن مصدر البيانات الذي يجب أن يستخدمه المدير JTA. يتم تقديم هذا المصدر في شكل اسم JNDI (Java Naming and Directory Interface). يتم تعريف مصدر البيانات هذا في [jboss-config.xml].

jboss-config.xml


<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">

    <!-- مصنع DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- اسم JNDI لـ DataSource -->
        <property name="jndiName">java:/datasource</property>

        <!-- قاعدة البيانات المدارة -->
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="connectionURL">jdbc:mysql://localhost:3306/jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>

        <!-- خصائص تجمع الاتصالات -->
        <property name="minSize">0</property>
        <property name="maxSize">10</property>
        <property name="blockingTimeout">1000</property>
        <property name="idleTimeout">100000</property>

        <!-- مدير المعاملات، هنا JTA -->
        <property name="transactionManager">
            <inject bean="TransactionManager" />
        </property>
        <!-- مدير ذاكرة التخزين المؤقتة Hibernate -->
        <property name="cachedConnectionManager">
            <inject bean="CachedConnectionManager" />
        </property>
        <!-- خصائص إنشاء مثيل JNDI؟ -->
        <property name="initialContextProperties">
            <inject bean="InitialContextProperties" />
        </property>
    </bean>

    <!-- يُطلب DataSource من مصنع -->
    <bean name="datasource" class="java.lang.Object">
        <constructor factoryMethod="getDatasource">
            <factory bean="datasourceFactory" />
        </constructor>
    </bean>

</deployment>
  • السطر 3: العلامة الجذرية للملف هي <deployment>. يهدف ملف النشر هذا بشكل أساسي إلى تكوين مصدر البيانات java:/datasource الذي تم إعلانه في persistence.xml.
  • يتم تعريف مصدر البيانات بواسطة bean "datasource" في السطر 38. نلاحظ أن مصدر البيانات يتم الحصول عليه (السطر 40) من "مصنع" تم تعريفه بواسطة bean "datasourceFactory" في السطر 7. للحصول على مصدر بيانات التطبيق، سيتعين على العميل استدعاء الطريقة [getDatasource] من factory (السطر 39).
  • السطر 7: factory التي توفر مصدر البيانات هي فئة Jboss.
  • السطر 9: اسم مصدر البيانات JNDI. يجب أن يكون هذا الاسم هو نفسه المعلن في العلامة <jta-data-source> في الملف persistence.xml. في الواقع، ستستخدم الطبقة JPA هذا الاسم JNDI لطلب مصدر البيانات.
  • الأسطر 12-15: شيء أكثر تقليدية: خصائص Jdbc للاتصال بـ SGBD
  • الأسطر 18-21: تكوين مجموعة الاتصالات الداخلية لحاوية Jboss Ejb3.
  • الأسطر 24-26: المدير JTA. يتم تعريف الفئة [TransactionManager] التي تم حقنها في السطر 25 في الملف [embedded-jboss-beans.xml].
  • الأسطر 28-30: ذاكرة التخزين المؤقتة Hibernate، وهو مفهوم لم نتطرق إليه. الفئة [CachedConnectionManager] التي تم إدخالها في السطر 29 محددة في الملف [embedded-jboss-beans.xml]. تجدر الإشارة إلى أن التكوين أصبح الآن يعتمد على Hibernate، مما سيشكل مشكلة لنا عندما نرغب في الترحيل إلى Toplink.
  • الأسطر 32-34: تكوين الخدمة JNDI.

لقد انتهينا من ملف تكوين Jboss Ejb3. إنه معقد ولا تزال هناك الكثير من الأمور الغامضة. وقد تم استخراجه من [ref1]. ومع ذلك، سنتمكن من تكييفه مع SGBD آخر (الأسطر 12-15 من jboss-config.xml، السطر 24 من persistence.xml). لم يكن الترحيل إلى Toplink ممكنًا لعدم وجود أمثلة.

3.2.6. برنامج العميل [InitDB]

سنبدأ بكتابة أول عميل للبنية التي تم وصفها سابقًا:

رمز [InitDB] هو التالي:


package tests;

...
public class InitDB {

    // طبقة الخدمة
    private static IService service;

    // المنشئ
    public static void main(String[] args) throws ParseException, NamingException {
        // يتم تشغيل الحاوية EJB3 JBoss
        // يتم استخدام ملفات التكوين ejb3-interceptors-aop.xml و embedded-jboss-beans.xml
        EJB3StandaloneBootstrap.boot(null);

        // إنشاء حبوب خاصة بالتطبيق
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // نشر جميع ملفات EJBs الموجودة في مسار الفئات (بطيء، يقوم بمسح كل شيء)
        // EJB3StandaloneBootstrap.scanClasspath();

        // يتم نشر جميع ملفات EJB الموجودة في مسار فئات التطبيق
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // يتم تهيئة السياق JNDI. يتم استخدام الملف jndi.properties
        InitialContext initialContext = new InitialContext();

        // إنشاء مثيل لطبقة الخدمة
        service = (IService) initialContext.lookup("Service/local");
        // يتم إفراغ قاعدة البيانات
        clean();
        // يتم ملؤها
        fill();
        // التحقق بصريًا
        dumpPersonnes();
        // إيقاف حاوية Ejb
        EJB3StandaloneBootstrap.shutdown();

    }

    // عرض محتوى الجدول
    private static void dumpPersonnes() {
        System.out.format("[personnes]-------------------------------------------------------------------%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // ملء الجدول
    public static void fill() throws ParseException {
        // إنشاء الأشخاص
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // يتم الحفظ
        service.saveArray(new Personne[] { p1, p2 });
    }

    // حذف عناصر من الجدول
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • تم العثور على طريقة تشغيل حاوية Jboss Ejb3 في [ref1].
  • السطر 13: يتم تشغيل الحاوية. [EJB3StandaloneBootstrap] هي فئة من فئات الحاوية.
  • السطر 16: يتم نشر وحدة النشر التي تم تكوينها بواسطة [jboss-config.xml] في الحاوية: يتم إعداد مدير JTA، ومصدر البيانات، ومجموعة الاتصالات، وذاكرة التخزين المؤقتة Hibernate، وخدمة JNDI.
  • السطر 22: يُطلب من الحاوية مسح مجلد bin الخاص بمشروع Eclipse للعثور على EJBs. سيتم العثور على EJBs الخاصة بالطبقات [service] و [dao] وإدارتها بواسطة الحاوية.
  • السطر 25: يتم تهيئة سياق JNDI. سيُستخدم هذا السياق لتحديد موقع EJBs.
  • السطر 28: يتم طلب EJB المطابق للفئة [Service] من الطبقة [service] من الخدمة JNDI. يمكن الوصول إلى Ejb محليًا (local) أو عبر الشبكة (remote). هنا يشير الاسم "Service/local" لـ Ejb المطلوب إلى الفئة [Service] من الطبقة [service] للوصول المحلي.
  • الآن، تم نشر التطبيق ولدينا مرجع على الطبقة [service]. نحن في نفس الموقف الذي يلي السطر 11 أدناه من كود [InitDB] من إصدار Spring. نجد إذن نفس الكود في كلا الإصدارين.

public class InitDB {

    // طبقة الخدمة
    private static IService service;

    // المنشئ
    public static void main(String[] args) throws ParseException {
        // تكوين التطبيق
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // طبقة الخدمة
        service = (IService) ctx.getBean("service");
        // إفراغ قاعدة البيانات
        clean();
        // ملء القاعدة
        fill();
        // التحقق بصريًا
        dumpPersonnes();
    }
...
  • السطر 36 (Jboss Ejb3): يتم إيقاف حاوية Ejb3.

يؤدي تنفيذ [InitDB] إلى النتائج التالية:

16:07:00,781  INFO LocalTxDataSource:117 - Bound datasource to JNDI name 'java:/datasource'
...
16:07:01,171  INFO Version:94 - Hibernate EntityManager 3.2.0.CR1
...
16:07:01,296  INFO Ejb3Configuration:94 - Processing PersistenceUnitInfo [
    name: jpa
    ...]
16:07:01,312  INFO Ejb3Configuration:94 - found EJB3 Entity bean: entites.Personne
...
16:07:01,375  INFO Configuration:94 - Reading mappings from resource: META-INF/orm.xml
16:07:01,375  INFO Ejb3Configuration:94 - [PersistenceUnit: jpa] no META-INF/orm.xml found
16:07:01,421  INFO AnnotationBinder:94 - Binding entity from annotated class: entites.Personne
16:07:01,468  INFO EntityBinder:94 - Bind entity entites.Personne on table jpa01_hb_personne
...
16:07:01,859  INFO SettingsFactory:94 - RDBMS: MySQL, version: 5.0.41-community-nt
16:07:01,859  INFO SettingsFactory:94 - JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-5.0.5 ( $Date: 2007-03-01 00:01:06 +0100 (Thu, 01 Mar 2007) $, $Revision: 6329 $ )
16:07:01,890  INFO Dialect:94 - Using dialect: org.hibernate.dialect.MySQLInnoDBDialect
16:07:01,890  INFO TransactionFactoryFactory:94 - Transaction strategy: org.hibernate.ejb.transaction.JoinableCMTTransactionFactory
...
16:07:02,234  INFO SchemaExport:94 - Running hbm2ddl schema export
16:07:02,234  INFO SchemaExport:94 - exporting generated schema to database
16:07:02,343  INFO SchemaExport:94 - schema export complete
...
16:07:02,562  INFO EJBContainer:479 - STARTED EJB: dao.Dao ejbName: Dao
...
16:07:02,593  INFO EJBContainer:479 - STARTED EJB: service.Service ejbName: Service
...
[personnes]-------------------------------------------------------------------
[1,0,p1,Paul,31/01/2000,true,2]
[2,0,p2,Sylvie,05/07/2001,false,0]

يُطلب من القارئ قراءة هذه السجلات. نجد فيها معلومات مثيرة للاهتمام حول ما يفعله حاوية Ejb3.

3.2.7. اختبارات وحدة [TestNG]

رمز البرنامج [TestNG] هو التالي:


package tests;

...
public class TestNG {

    // طبقة الخدمة
    private IService service = null;

    @BeforeClass
    public void init() throws NamingException, ParseException {
        // سجل
        log("init");
        // تشغيل الحاوية EJB3 JBoss
        // يتم استغلال ملفات التكوين ejb3-interceptors-aop.xml و embedded-jboss-beans.xml
        EJB3StandaloneBootstrap.boot(null);

        // إنشاء حبوب خاصة بالتطبيق
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // نشر جميع ملفات EJBs الموجودة في مسار الفئات (بطيء، يقوم بمسح الكل)
        // EJB3StandaloneBootstrap.scanClasspath();

        // يتم نشر جميع ملفات EJB الموجودة في مسار فئات التطبيق
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // يتم تهيئة السياق JNDI. يتم استخدام الملف jndi.properties
        InitialContext initialContext = new InitialContext();

        // إنشاء مثيل لطبقة الخدمة
        service = (IService) initialContext.lookup("Service/local");
        // يتم إفراغ قاعدة البيانات
        clean();
        // يتم ملؤها
        fill();
        // التحقق بصريًا
        dumpPersonnes();
    }

    @AfterClass
    public void terminate() {
        // السجل
        log("terminate");
        // إيقاف تشغيل حاوية EJB
        EJB3StandaloneBootstrap.shutdown();
    }

    @BeforeMethod
    public void setUp() throws ParseException {
...
    }

...
}
  • تستخدم طريقة init (الأسطر 10-37) لتهيئة البيئة اللازمة للاختبارات، وتستند إلى الكود الموضح سابقًا في [InitDB].
  • تقوم طريقة terminate (الأسطر 40-45) التي يتم تنفيذها في نهاية الاختبارات (وجود التعليق التوضيحي @AfterClass) بإيقاف الحاوية Ejb3 (السطر 44).
  • كل ما عدا ذلك مطابق لما كان عليه في إصدار Spring.

نجحت الاختبارات:

Image

3.2.8. تغيير SGBD

لتغيير SGBD، ما عليك سوى استبدال محتوى المجلد [META-INF] [2] بمحتوى المجلد SGBD الموجود في المجلد [conf] [1]. لنأخذ مثالاً على خادم SQL:

الملف [persistence.xml] هو التالي:


<persistence 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_1_0.xsd" version="1.0">

    <persistence-unit name="jpa">

        <!-- المزود JPA هو Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- تدير بيئة Java DataSource JTA -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- البحث عن كيانات الطبقة JBA -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- سجلات SQL Hibernate
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- نوع SGBD المدار -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />

            <!-- إعادة إنشاء جميع الجداول (drop+create) عند نشر وحدة الاستمرارية -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

لم يتغير سوى سطر واحد:

  • السطر 24: اللهجة SQL التي يجب أن يستخدمها Hibernate

أما الملف [jboss-config.xml] الخاص بخادم SQL فهو كما يلي:


<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">

    <!-- مصنع لـ DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- اسم JNDI لـ DataSource -->
        <property name="jndiName">java:/datasource</property>

        <!-- قاعدة البيانات المدارة -->
        <property name="driverClass">com.microsoft.sqlserver.jdbc.SQLServerDriver</property>
        <property name="connectionURL">jdbc:sqlserver://localhost\\SQLEXPRESS:1246;databaseName=jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>

        <!-- خصائص تجمع الاتصالات -->
    ...
    </bean>

</deployment>

تم تغيير الأسطر 12-15 فقط: وهي تحدد خصائص اتصال Jdbc الجديد.

يُطلب من القارئ تكرار الاختبارات الموصوفة لـ MySQL5 مع ملفات SGBD الأخرى.

3.2.9. تغيير التنفيذ JPA

كما ذكرنا أعلاه، لم نجد أي مثال على استخدام حاوية Jboss Ejb3 مع Toplink. حتى الآن (يونيو 2007)، ما زلت لا أعرف ما إذا كان هذا التكوين ممكنًا أم لا.

3.3. أمثلة أخرى

لنلخص ما تم إنجازه مع الكيان Personne. قمنا ببناء ثلاث بنى لإجراء نفس الاختبارات:

1 - تطبيق Spring / Hibernate

2 - تطبيق Spring / Toplink

3 - تطبيق Jboss Ejb3 / Hibernate

تتضمن أمثلة البرنامج التعليمي هذه البنى الثلاث مع كيانات أخرى تمت دراستها في الجزء الأول من البرنامج التعليمي:

الفئة - المقالة

  • في [1]: إصدار Spring / Hibernate
  • في [2]: إصدار Spring / Toplink
  • في [3]: إصدار Jboss Ejb3 / Hibernate

الشخص - العنوان - النشاط

  • في [1]: إصدار Spring / Hibernate
  • في [2]: إصدار Spring / Toplink
  • في [3]: إصدار Jboss Ejb3 / Hibernate

لا تضيف هذه الأمثلة أي جديد فيما يتعلق بالبنية. إنها تندرج ببساطة في سياق حيث توجد عدة كيانات يجب إدارتها مع علاقات واحد إلى عدة أو عدة إلى عدة فيما بينها، وهو ما لم يكن موجودًا في الأمثلة مع الكيان Personne.

3.4. المثال 3: Spring / JPA في تطبيق ويب

3.4.1. مقدمة

نستعرض هنا تطبيقًا تم تقديمه في الوثيقة التالية:

[ref4]: أساسيات تطوير الويب MVC بلغة Java [http://tahe.developpez.com/java/baseswebmvc/].

يقدم هذا المستند أساسيات تطوير الويب MVC بلغة Java. لفهم المثال التالي، يجب أن يكون لدى القارئ هذه الأساسيات. ستستخدم تطبيق الويب خادم Tomcat. يتم عرض تثبيته واستخدامه داخل Eclipse في الفقرة 5.3.

تم تطوير التطبيق باستخدام طبقة [dao] تعتمد على أداة Ibatis / SqlMap [http://ibatis.apache.org/] التي كانت تضمن الجسر بين العلاقات والكائنات. نكتفي باستبدال Ibatis بـ JPA. ستكون بنية التطبيق كما يلي:

سيسمح التطبيق الويب الذي سنقوم بكتابته بإدارة مجموعة من الأشخاص من خلال أربع عمليات:

  • قائمة بأفراد المجموعة
  • إضافة شخص إلى المجموعة
  • تعديل شخص من المجموعة
  • حذف شخص من المجموعة

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

 

3.4.2. مشروع Eclipse

مشروع Eclipse للتطبيق هو كما يلي:

  • في [1]: مشروع الويب. إنه مشروع Eclipse من النوع [Dynamic Web Project] [2]. يمكن العثور عليه في [4] في مجلد [3] الخاص بأمثلة البرنامج التعليمي. سنقوم باستيراده.
  • في [5]: المصادر وتكوين الطبقات [service, dao, jpa]. نحتفظ بما تم إنجازه [dao, entites, service] من مشروع Eclipse [hibernate-spring-personnes-metier-dao] الذي تمت دراسته في الفقرة 3.1.1. نقوم فقط بتطوير الطبقة [web] الممثلة هنا بالحزمة [web]. بالإضافة إلى ذلك، نحتفظ بملفات التكوين [persistence.xml, spring-config.xml] لهذا المشروع مع تفصيل بسيط وهو أننا سنستخدم SGBD Postgres، مما يترجم إلى التعديلات التالية في [spring-config.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
...
                <property name="databasePlatform" value="org.hibernate.dialect.PostgreSQLDialect" />
...
        </property>
    ...
    </bean>

    <!-- مصدر البيانات DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql:jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
....
</beans>

تم تكييف الأسطر 8 و16-19 مع Postgres.

  • في [6]: يحتوي المجلد [WebContent] على صفحات JSP الخاصة بالمشروع بالإضافة إلى المكتبات اللازمة. يتم عرض هذه المكتبات في [8]
  • يمكن استخدام التطبيق مع مختلف SGBD. يكفي تغيير الملف [spring-config.xml]. يحتوي المجلد [conf] [7] على الملف [spring-config.xml] الملائم لمختلف SGBD.

3.4.3. الطبقة [web]

تتميز تطبيقاتنا بالبنية متعددة الطبقات التالية:

ستعرض الطبقة [web] شاشات للمستخدم لتمكينه من إدارة مجموعة الأشخاص:

  • قائمة بأفراد المجموعة
  • إضافة شخص إلى المجموعة
  • تعديل شخص في المجموعة
  • حذف شخص من المجموعة

ولتحقيق ذلك، ستعتمد على الطبقة [service] التي ستستعين بدورها بالطبقة [dao]. لقد قدمنا بالفعل الشاشات التي تديرها الطبقة [web] (الفقرة 3.4.1). لوصف طبقة الويب، سنقدم بالتتابع:

  • تكوينها
  • طرق العرض
  • وحدة التحكم الخاصة بها
  • بعض الاختبارات

3.4.3.1. تكوين تطبيق الويب

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

 
  • في الحزمة [web]، نجد وحدة التحكم في تطبيق الويب: الفئة [Application].
  • توجد صفحات JSP / JSTL الخاصة بالتطبيق في [WEB-INF/vues].
  • يحتوي المجلد [WEB-INF/lib] على الأرشيفات الخارجية اللازمة للتطبيق. وهي مرئية في المجلد [Web App Libraries].

[web.xml]


الملف [web.xml] هو الملف الذي يستخدمه خادم الويب لتحميل التطبيق. ومحتواه كما يلي:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>spring-jpa-hibernate-personnes-crud</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personnes</servlet-name>
        <servlet-class>web.Application</servlet-class>
        <init-param>
            <param-name>urlEdit</param-name>
            <param-value>/WEB-INF/vues/edit.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/WEB-INF/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlList</param-name>
            <param-value>/WEB-INF/vues/list.jsp</param-value>
        </init-param>
    </servlet>
    <!--  التعيين ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  ملفات الصفحة الرئيسية -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  صفحة خطأ غير متوقع -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • الأسطر 23-26: سيتم معالجة عناوين URL [/do/*] بواسطة السيرفلت [personnes]
  • السطور 7-8: الخادم الصغير [personnes] هو مثيل لفئة [Application]، وهي فئة سنقوم بإنشائها.
  • الأسطر 9-20: تحدد ثلاثة معلمات [urlList, urlEdit, urlErreurs] تحدد عناوين URL لصفحات JSP الخاصة بعروض [list, edit, erreurs].
  • الأسطر 28-30: يحتوي التطبيق على صفحة دخول افتراضية [index.jsp] تقع في جذر مجلد تطبيق الويب.
  • الأسطر 32-35: يحتوي التطبيق على صفحة أخطاء افتراضية يتم عرضها عندما يلتقط خادم الويب استثناءً لا يديره التطبيق.
    • السطر 37: تشير العلامة <exception-type> إلى نوع الاستثناء الذي تديره التوجيهية <error-page>، وهنا النوع [java.lang.Exception] ومشتقاته، أي جميع الاستثناءات.
    • السطر 38: تشير العلامة <location> إلى الصفحة JSP التي يجب عرضها عند حدوث استثناء من النوع المحدد بواسطة <exception-type>. الاستثناء الذي حدث متاح في هذه الصفحة في كائن يسمى exception إذا كانت الصفحة تحتوي على التوجيه:

<%@ page isErrorPage="true" %>
  • (تابع)
    • إذا حدد <exception-type> نوعًا T1 ووصلت استثناءات من النوع T2 غير مشتقة من T1 إلى خادم الويب، فإن الخادم يرسل إلى العميل صفحة استثناء خاصة به، وهي عادةً ما تكون غير سهلة الاستخدام. ومن هنا تأتي أهمية العلامة <error-page> في الملف [web.xml].

[index.jsp]


يتم عرض هذه الصفحة إذا طلب المستخدم سياق التطبيق مباشرةً دون تحديد عنوان URL، c.a.d. هنا [/spring-jpa-hibernate-personnes-crud]. ومحتواها كما يلي:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/do/list"/>

يقوم [index.jsp] (السطر 4) بإعادة توجيه العميل إلى عنوان URL [/do/list]. يعرض عنوان URL هذا قائمة بأعضاء المجموعة.

3.4.3.2. صفحات JSP / JSTL للتطبيق


عرض [list.jsp]


يُستخدم لعرض قائمة الأشخاص:

Image

رمزها هو التالي:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
            <c:if test="${erreurs!=null}">
                <h3>Les erreurs suivantes se sont produites :</h3>
                <ul>
                    <c:forEach items="${erreurs}" var="erreur">
                        <li><c:out value="${erreur}"/></li>
                    </c:forEach>
                </ul>
            <hr>
        </c:if>
        <h2>Liste des personnes</h2>
        <table border="1">
            <tr>
                <th>Id</th>
                <th>Version</th>
                <th>Pr&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</th>
                <th>Nombre d'enfants</th>
                <th></th>
            </tr>
            <c:forEach var="personne" items="${personnes}">
                <tr>
                    <td><c:out value="${personne.id}"/></td>
                    <td><c:out value="${personne.version}"/></td>
                    <td><c:out value="${personne.prenom}"/></td>
                    <td><c:out value="${personne.nom}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${personne.datenaissance.time}</dt:format></td>
                    <td><c:out value="${personne.marie}"/></td>
                    <td><c:out value="${personne.nbenfants}"/></td>
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>
  • تستقبل هذه العرضة عنصرين في نموذجها:
    • العنصر [personnes] المرتبط بكائن من النوع [List] من كائنات من النوع [Personne]: قائمة بالأشخاص.
    • العنصر الاختياري [erreurs] المرتبط بكائن من النوع [List] من كائنات من النوع [String]: قائمة برسائل الخطأ.
  • الأسطر 31-43: يتم تصفح قائمة ${personnes} لعرض جدول HTML يحتوي على الأشخاص في المجموعة.
  • السطر 40: يتم تعيين عنوان URL الذي يشير إليه الرابط [Modifier] بواسطة الحقل [id] للشخص الحالي حتى يعرف المتحكم المرتبط بعنوان URL [/do/edit] الشخص الذي يجب تعديله.
  • السطر 41: يتم إجراء الأمر نفسه بالنسبة للرابط [Supprimer].
  • السطر 37: لعرض تاريخ ميلاد الشخص بالصيغة JJ/MM/AAAA، نستخدم العلامة <dt> من مكتبة العلامات [DateTime] لمشروع Apache [Jakarta Taglibs]:

Image

ملف وصف مكتبة العلامات هذه محدد في السطر 3.

  • السطر 46: الرابط [Ajout] الخاص بإضافة شخص جديد يستهدف عنوان URL [/do/edit] مثل الرابط [Modifier] في السطر 40. القيمة -1 للمعلمة [id] هي التي تشير إلى أننا نتعامل مع عملية إضافة وليس تعديل.
  • السطور 10-18: إذا كان العنصر ${erreurs} موجودًا في النموذج، فسيتم عرض رسائل الخطأ التي يحتوي عليها.

الطريقة [edit.jsp]


يُستخدم لعرض نموذج إضافة شخص جديد أو تعديل شخص موجود:

رمز العرض [edit.jsp] هو التالي:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="../ressources/standard.jpg">
        <h2>Ajout/Modification d'une personne</h2>
        <c:if test="${erreurEdit!=''}">
            <h3>Echec de la mise à jour :</h3>
          L'erreur suivante s'est produite : ${erreurEdit}
            <hr>
        </c:if>
        <form method="post" action="<c:url value="/do/validate"/>">
            <table border="1">
                <tr>
                    <td>Id</td>
                    <td>${id}</td>
                </tr>
                <tr>
                    <td>Version</td>
                    <td>${version}</td>
                </tr>
                <tr>
                    <td>Pr&eacute;nom</td>
                    <td>
                        <input type="text" value="${prenom}" name="prenom" size="20">
                    </td>
                    <td>${erreurPrenom}</td>
                </tr>
                <tr>
                    <td>Nom</td>
                    <td>
                        <input type="text" value="${nom}" name="nom" size="20">
                    </td>
                    <td>${erreurNom}</td>
                </tr>
                <tr>
                <td>Date de naissance (JJ/MM/AAAA)</td>
                    <td>
                        <input type="text" value="${datenaissance}" name="datenaissance">
                    </td>
                    <td>${erreurDateNaissance}</td>
                </tr>
                <tr>
                    <td>Mari&eacute;</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Nombre d'enfants</td>
                    <td>
                        <input type="text" value="${nbenfants}" name="nbenfants">
                    </td>
                    <td>${erreurNbEnfants}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>
    </body>
</html>

تعرض هذه العرض نموذجًا لإضافة شخص جديد أو تحديث شخص موجود. بعد ذلك، ولتبسيط الكتابة، سنستخدم المصطلح الوحيد [mise à jour]. يؤدي الزر [Valider] (السطر 73) إلى تشغيل POST للنموذج على عنوان URL [/do/validate] (السطر 16). إذا فشل POST، يتم إعادة عرض العرض [edit.jsp] مع الخطأ أو الأخطاء التي حدثت، وإلا يتم عرض العرض [list.jsp].

  • تتلقى طريقة العرض [edit.jsp] المعروضة على كل من GET و POST التي تفشل، العناصر التالية في نموذجها:
السمة
GET
POST
id
معرف الشخص الذي تم تحديثه
المحدثة
نفس الشيء
version
إصداره
كما هو
prenom
اسمه الأول
الاسم الأول الذي تم إدخاله
nom
اسمه
الاسم المدخل
datenaissance
تاريخ الميلاد
تاريخ الميلاد الذي تم إدخاله
marie
حالته الاجتماعية
الحالة الاجتماعية التي تم إدخالها
nbenfants
عدد أطفاله
عدد الأطفال الذي تم إدخاله
erreurEdit
فارغ
رسالة خطأ تشير إلى فشل الإضافة
أو التعديل في وقت POST الذي تم إجراؤه
بواسطة الزر [Envoyer]. فارغ في حالة عدم وجود خطأ.
erreurPrenom
فارغ
يشير إلى اسم أول خاطئ – فارغ بخلاف ذلك
erreurNom
فارغ
يشير إلى اسم خاطئ – فارغ بخلاف ذلك
erreurDateNaissance
فارغ
يشير إلى تاريخ ميلاد خاطئ – فارغ بخلاف ذلك
erreurNbEnfants
فارغ
يشير إلى عدد أطفال خاطئ – فارغ بخلاف ذلك
  • الأسطر 11-15: إذا فشل POST في النموذج، فسيظهر [erreurEdit!=''] وستظهر رسالة خطأ.
  • السطر 16: سيتم إرسال النموذج إلى عنوان URL [/do/validate]
  • السطر 20: يتم عرض عنصر [id] من القالب
  • السطر 24: يتم عرض العنصر [version] من النموذج
  • الأسطر 26-32: إدخال الاسم الأول للشخص:
    • عند العرض الأولي للنموذج (GET)، يعرض ${prenom} القيمة الحالية لحقل [prenom] الخاص بالكائن [Personne] الذي تم تحديثه ويكون ${erreurPrenom} فارغًا.
    • في حالة حدوث خطأ بعد POST، يتم إعادة عرض القيمة المدخلة ${prenom} بالإضافة إلى رسالة الخطأ المحتملة ${erreurPrenom}
  • الأسطر 33-39: إدخال اسم الشخص
  • الأسطر 40-46: إدخال تاريخ ميلاد الشخص
  • الأسطر 47-61: إدخال الحالة الاجتماعية للشخص (متزوج أم لا) باستخدام زر اختيار. يتم استخدام قيمة الحقل [marie] من الكائن [Personne] لمعرفة أي من زري الاختيار يجب تحديده.
  • السطور 62-68: إدخال عدد أطفال الشخص
  • السطر 71: حقل HTML مخفي باسم [id] وقيمته هي الحقل [id] للشخص قيد التحديث، -1 للإضافة، وقيمة أخرى للتعديل.
  • السطر 72: حقل مخفي HTML باسم [version] وقيمته هي الحقل [id] للشخص قيد التحديث.
  • السطر 73: الزر [Valider] من النوع [Submit] في النموذج
  • السطر 74: رابط يسمح بالعودة إلى قائمة الأشخاص. وقد تمت تسميته [Annuler] لأنه يسمح بمغادرة النموذج دون تأكيده.

عرض [exception.jsp]


تُستخدم لعرض صفحة تشير إلى حدوث استثناء لم يتم التعامل معه من قبل التطبيق وتم إرساله إلى خادم الويب.

على سبيل المثال، لنحذف شخصًا غير موجود في المجموعة:

رمز عرض [exception.jsp] هو التالي:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>

<%
  response.setStatus(200);
%>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>MVC - personnes</h2>
        L'exception suivante s'est produite :
        <%= exception.getMessage()%>
        <br><br>
        <a href="<c:url value="/do/list"/>">Retour &agrave; la liste</a>
    </body>
</html>
  • تتلقى هذه العرضة مفتاحًا في نموذجها، وهو العنصر [exception] الذي يمثل الاستثناء الذي تم اعتراضه بواسطة خادم الويب. لكي يتم تضمين هذا العنصر في نموذج الصفحة JSP بواسطة خادم الويب، يجب أن تكون الصفحة قد حددت العلامة في السطر 3.
  • السطر 6: يتم تعيين رمز حالة الاستجابة HTTP على 200. هذا هو الرأس الأول HTTP للاستجابة. يشير الرمز 200 للعميل إلى أن طلبه قد تم تلبية. عادةً ما يتم تضمين مستند HTML في استجابة الخادم. وهذا هو الحال هنا. إذا لم يتم تعيين رمز الحالة HTTP للرد على 200، فسيكون له هنا القيمة 500 مما يعني حدوث خطأ. في الواقع، عندما يعترض خادم الويب استثناءً غير مُعالج، فإنه يرى هذه الحالة غير طبيعية ويبلغ عنها برمز 500. يختلف رد الفعل على الرمز 500 باختلاف المتصفحات: يعرض Firefox المستند الذي قد يصاحب هذه الاستجابة، بينما يتجاهل متصفح آخر هذا المستند ويعرض صفحته الخاصة. ولهذا السبب استبدلنا الرمز 500 بالرمز 200.
  • السطر 16: يتم عرض نص الاستثناء
  • السطر 18: يُعرض للمستخدم رابط للعودة إلى قائمة الأشخاص

عرض [erreurs.jsp]


يُستخدم لعرض صفحة تشير إلى أخطاء تهيئة التطبيق، c.a.d. الأخطاء المكتشفة أثناء تنفيذ طريقة [init] لبرنامج الخدمة الخاص بوحدة التحكم. قد يكون ذلك، على سبيل المثال، عدم وجود معلمة في الملف [web.xml] كما هو موضح في المثال أدناه:

Image

رمز الصفحة [erreurs.jsp] هو التالي:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
      <title>MVC - Personnes</title>
  </head>
  <body>
      <h2>Les erreurs suivantes se sont produites</h2>
    <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li>${erreur}</li>
            </c:forEach>
    </ul>
  </body>
</html>

تستقبل الصفحة في نموذجها عنصر [erreurs] وهو كائن من النوع [ArrayList] من كائنات [String]، وهذه الأخيرة هي رسائل خطأ. يتم عرضها بواسطة حلقة الأسطر 13-15.

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

يتم تعريف وحدة التحكم [Application] في الحزمة [web]:

Image


هيكل وبدء تشغيل وحدة التحكم


هيكل وحدة التحكم [Application] هو كما يلي:


package web;

...


@SuppressWarnings("serial")
public class Application extends HttpServlet {
    // معلمات المثيل
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

    // الخدمة
    private IService service = null;

    // init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // يتم استرداد معلمات التهيئة من السيرفلت
        ServletConfig config = getServletConfig();
        // معالجة معلمات التهيئة الأخرى
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
            // قيمة المعلمة
            valeur = config.getInitParameter(paramètres[i]);
            // هل المعلمة موجودة؟
            if (valeur == null) {
                // تسجيل الخطأ
                erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
            } else {
                // يتم حفظ قيمة المعلمة
                params.put(paramètres[i], valeur);
            }
        }
        // عنوان URL للطريقة [erreurs] له معالجة خاصة
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
        // تكوين التطبيق
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // طبقة الخدمة
        service = (IService) ctx.getBean("service");
        // يتم إفراغ قاعدة البيانات
        clean();
        // ملء قاعدة البيانات
        try {
            fill();
        } catch (ParseException e) {
            throw new ServletException(e);
        }
    }

    // ملء الجدول
    public void fill() throws ParseException {
        // إنشاء الأشخاص
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // يتم الحفظ
        service.saveArray(new Personne[] { p1, p2 });
    }

    // حذف عناصر من الجدول
    public void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }

    // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
...
    }

    // عرض قائمة الأشخاص
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // تعديل / إضافة شخص
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // حذف شخص
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // تأكيد تعديل / إضافة شخص
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // عرض النموذج المعبأ مسبقًا
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
    ...
    }

    // إرسال
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // ننتقل إلى GET
        doGet(request, response);
    }

}
  • الأسطر 21-34: يتم استرداد المعلمات المتوقعة في الملف [web.xml].
  • الأسطر 37-39: يجب أن يكون المعلمة [urlErreurs] موجودة بالضرورة لأنها تشير إلى عنوان URL للطريقة [erreurs] القادرة على عرض أي أخطاء في التهيئة. إذا لم يكن موجودًا، يتم إيقاف التطبيق عن طريق تشغيل [ServletException] (السطر 39). سيتم إرسال هذا الاستثناء إلى خادم الويب وسيتم التعامل معه بواسطة العلامة <error-page> في الملف [web.xml]. وبالتالي يتم عرض العرض [exception.jsp]:

Image

الرابط [Retour à la liste] أعلاه غير فعال. يؤدي استخدامه إلى إرجاع نفس الاستجابة طالما لم يتم تعديل التطبيق وإعادة تحميله. وهو مفيد لأنواع أخرى من الاستثناءات كما رأينا سابقًا.

  • الأسطر 40-43: تستخدم ملف تكوين Spring لاسترداد مرجع على الطبقة [service]. بعد تهيئة وحدة التحكم، تتوفر لأساليبها مرجع [service] على الطبقة [service] (السطر 15) والتي ستستخدمها لتنفيذ الإجراءات التي يطلبها المستخدم. سيتم اعتراض هذه الإجراءات بواسطة الطريقة [doGet] التي ستقوم بمعالجتها بواسطة طريقة معينة في وحدة التحكم:
Url
طريقة HTTP
طريقة وحدة التحكم
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
GET
doDeletePersonne

الطريقة [doGet]


تهدف هذه الطريقة إلى توجيه معالجة الإجراءات التي يطلبها المستخدم إلى الطريقة الصحيحة. ورمزها هو التالي:


// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {

        
// التحقق من سير عملية تهيئة السيرفلت
        if (erreursInitialisation.size() != 0) {
            // ننتقل إلى صفحة الأخطاء
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
            // النهاية
            return;
        }
        // نسترد طريقة إرسال الطلب
        String méthode = request.getMethod().toLowerCase();
        // يتم استرداد الإجراء المطلوب تنفيذه
        String action = request.getPathInfo();
        // الإجراء؟
        if (action == null) {
            action = "/list";
        }
        // تنفيذ الإجراء
        if (méthode.equals("get") && action.equals("/list")) {
            // قائمة الأشخاص
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
            // حذف شخص
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
            // عرض نموذج إضافة/تعديل شخص
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
            // التحقق من صحة نموذج إضافة/تعديل شخص
            doValidatePersonne(request, response);
            return;
        }
        // حالات أخرى
        doListPersonnes(request, response);
    }
  • الأسطر 7-13: يتم التحقق من أن قائمة أخطاء التهيئة فارغة. إذا لم يكن الأمر كذلك، يتم عرض طريقة العرض [erreurs(erreurs)] التي ستشير إلى الخطأ أو الأخطاء.
  • السطر 15: يتم استرداد الطريقة [get] أو [post] التي استخدمها العميل لتقديم طلبه.
  • السطر 17: يتم استرداد قيمة المعلمة [action] من الطلب.
  • الأسطر 23-27: معالجة الطلب [GET /do/list] الذي يطلب قائمة بالأشخاص.
  • الأسطر 28-32: معالجة الطلب [GET /do/delete] الذي يطلب حذف شخص.
  • الأسطر 33-37: معالجة الطلب [GET /do/edit] الذي يطلب نموذج تحديث شخص.
  • الأسطر 38-42: معالجة الطلب [POST /do/validate] الذي يطلب التحقق من صحة البيانات المحدثة.
  • السطر 44: إذا لم تكن الإجراء المطلوب أحد الإجراءات الخمسة السابقة، فسيتم التعامل معه كما لو كان [GET /do/list].

الطريقة [doListPersonnes]


تعالج هذه الطريقة الطلب [GET /do/list] الذي يطلب قائمة بالأشخاص:

Image

وإليك كودها:


    // عرض قائمة الأشخاص
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // نموذج العرض [list]
        request.setAttribute("personnes", service.getAll());
        // عرض العرض [list]
        getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
  • السطر 4: نطلب من الطبقة [service] قائمة بالأشخاص في المجموعة ونضعها في النموذج تحت المفتاح "أشخاص".
  • السطر 6: يتم عرض العرض [list.jsp] الموصوف في الفقرة 3.4.3.2.

الطريقة [doDeletePersonne]


تعالج هذه الطريقة الاستعلام [GET /do/delete?id=XX] الذي يطلب حذف الشخص ذي المعرف id=XX. عنوان URL [/do/delete?id=XX] هو عنوان روابط [Supprimer] في العرض [list.jsp]:

Image

والذي يكون الرمز الخاص به كما يلي:


...
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

في السطر 12، نرى عنوان URL [/do/delete?id=XX] للرابط [Supprimer]. يجب أن تقوم الطريقة [doDeletePersonne] التي تعالج عنوان URL هذا بحذف الشخص ذي المعرف id=XX ثم عرض القائمة الجديدة لأفراد المجموعة. ورمزها هو التالي:


// حذف شخص
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // استرداد معرف الشخص
        int id = Integer.parseInt(request.getParameter("id"));
        // حذف الشخص
        service.deleteOne(id);
        // إعادة التوجيه إلى قائمة الأشخاص
        response.sendRedirect("list");
    }
  • السطر 4: عنوان URL المعالج هو [/do/delete?id=XX]. يتم استرداد القيمة [XX] من المعلمة [id].
  • السطر 6: نطلب من الطبقة [service] حذف الشخص الذي يحمل المعرف الذي تم الحصول عليه. لا نقوم بأي تحقق. إذا كان الشخص الذي نسعى إلى حذفه غير موجود، فإن الطبقة [dao] تطلق استثناءً ترفعه الطبقة [service]. نحن لا نديرها هنا أيضًا، في وحدة التحكم. وبالتالي، ستنتقل إلى خادم الويب الذي سيقوم، حسب التكوين، بعرض الصفحة [exception.jsp]، الموضحة في الفقرة 3.4.3.2:

Image

  • السطر 9: إذا تمت الإزالة (بدون استثناء)، يُطلب من العميل إعادة التوجيه إلى عنوان URL النسبي [list]. نظرًا لأن الرابط الذي تمت معالجته للتو هو [/do/delete]، فإن عنوان URL لإعادة التوجيه سيكون [/do/list]. وبالتالي، سيقوم المتصفح بإجراء [GET /do/list] مما سيؤدي إلى عرض قائمة الأشخاص.

الطريقة [doEditPersonne]


تعالج هذه الطريقة الطلب [GET /do/edit?id=XX] الذي يطلب نموذج تحديث الشخص ذي المعرف id=XX. عنوان URL [/do/edit?id=XX] هو عنوان روابط [Modifier] ورابط [Ajout] الخاص بعرض [list.jsp]:

Image

والذي يكون الرمز الخاص به كما يلي:


...
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

في السطر 11، نرى عنوان URL [/do/edit?id=XX] للرابط [Modifier] وفي السطر 17، عنوان URL [/do/edit?id=-1] للرابط [Ajout]. يجب أن تعرض الطريقة [doEditPersonne] نموذج تحرير الشخص ذي المعرف id=XX أو، إذا كان الأمر يتعلق بإضافة، أن تعرض نموذجًا فارغًا.

  • في [1] أعلاه، نموذج الإضافة وفي [2] نموذج التعديل.

رمز طريقة [doEditPersonne] هو التالي:


// تعديل / إضافة شخص
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // استرداد معرف الشخص
        int id = Integer.parseInt(request.getParameter("id"));
        // إضافة أم تعديل؟
        Personne personne = null;
        if (id != -1) {
            // تعديل - استرداد الشخص المراد تعديله
            personne = service.getOne(id);
            request.setAttribute("id", personne.getId());
            request.setAttribute("version", personne.getVersion());
        } else {
            // إضافة - يتم إنشاء سجل شخص فارغ
            personne = new Personne();
            request.setAttribute("id", -1);
            request.setAttribute("version", -1);
        }
        // نضع الكائن [Personne] في جلسة عمل المستخدم
        request.getSession().setAttribute("personne", personne);
        // وفي نموذج العرض [edit]
        request.setAttribute("erreurEdit", "");
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDatenaissance();
        if (dateNaissance != null) {
            request.setAttribute("datenaissance", new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("datenaissance", "");
        }
        request.setAttribute("marie", personne.isMarie());
        request.setAttribute("nbenfants", personne.getNbenfants());
        // عرض العرض [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • يستهدف GET عنوان URL من النوع [/do/edit?id=XX]. في السطر 4، نسترد قيمة [id]. بعد ذلك، هناك حالتان:
    1. إذا كان id مختلفًا عن -1، فهذا يعني أنه تعديل ويجب عرض نموذج مملوء مسبقًا بمعلومات الشخص المراد تعديله. في السطر 9، يتم طلب هذا الشخص من الطبقة [service].
    2. id يساوي -1. إذن، يتعلق الأمر بإضافة ويجب عرض نموذج فارغ. لهذا الغرض، يتم إنشاء شخص فارغ في السطر 14.
    3. في كلتا الحالتين، يتم تهيئة العناصر [id, version] من نموذج الصفحة [edit.jsp] الموصوفة في الفقرة 3.4.3.2.
  • يتم وضع الكائن [Personne] الناتج في قالب الصفحة [edit.jsp]. يتضمن هذا القالب العناصر التالية [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, datenaissance, erreurDateNaissance, marie, nbenfants, erreurNbEnfants]. يتم تهيئة هذه العناصر في الأسطر 19-31 باستثناء تلك التي تكون قيمتها هي السلسلة الفارغة [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. ومن المعروف أنه في حالة عدم وجودها في النموذج، ستعرض المكتبة JSTL سلسلة فارغة كقيمة لها. على الرغم من أن العنصر [erreurEdit] له أيضًا قيمة سلسلة فارغة، إلا أنه يتم تهيئته لأن هناك اختبارًا يتم إجراؤه على قيمته في الصفحة [edit.jsp].
  • بمجرد أن يصبح النموذج جاهزًا، ينتقل التحكم إلى الصفحة [edit.jsp]، السطر 33، والتي ستقوم بإنشاء العرض [edit].

الطريقة [doValidatePersonne]


تعالج هذه الطريقة الطلب [POST /do/validate] الذي يتحقق من صحة نموذج التحديث. يتم تشغيل POST بواسطة الزر [Valider]:

Image

دعونا نستعرض عناصر الإدخال في النموذج HTML من العرض أعلاه:


<form method="post" action="<c:url value="/do/validate"/>">
...
                        <input type="text" value="${nom}" name="nom" size="20">
...
                        <input type="text" value="${datenaissance}" name="datenaissance">
...
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
...
                        <input type="text" value="${nbenfants}" name="nbenfants">
...
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>

يحتوي الطلب POST على المعلمات [prenom, nom, datenaissance, marie, nbenfants, id] ويتم إرساله إلى عنوان URL [/do/validate] (السطر 1). ويتم معالجته بواسطة الطريقة [doValidatePersonne] التالية:


// التحقق من صحة تعديل/إضافة شخص
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // استرداد العناصر التي تم إرسالها
        boolean formulaireErroné = false;
        boolean erreur;
        // الاسم الأول
        String prenom = request.getParameter("prenom").trim();
        // هل الاسم الأول صالح؟
        if (prenom.length() == 0) {
            // تسجيل الخطأ
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
        // اللقب
        String nom = request.getParameter("nom").trim();
        // هل الاسم الأول صالح؟
        if (nom.length() == 0) {
            // يتم تسجيل الخطأ
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
        // تاريخ الميلاد
        Date datenaissance = null;
        try {
            datenaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request.getParameter("datenaissance").trim());
        } catch (ParseException e) {
            // يتم تسجيل الخطأ
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
        // الحالة الاجتماعية
        boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
        // عدد الأطفال
        int nbenfants = 0;
        erreur = false;
        try {
            nbenfants = Integer.parseInt(request.getParameter("nbenfants").trim());
            if (nbenfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
            // يُلاحظ الخطأ
            erreur = true;
        }
        // عدد الأطفال غير صحيح؟
        if (erreur) {
            // تم الإبلاغ عن الخطأ
            request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
        // رقم تعريف الشخص
        int id = Integer.parseInt(request.getParameter("id"));
        // هل النموذج غير صحيح؟
        if (formulaireErroné) {
            // إعادة عرض النموذج مع رسائل الخطأ
            showFormulaire(request, response, "");
            // انتهى
            return;
        }
        // النموذج صحيح - يتم تحديث الشخص الذي تم وضعه في الجلسة
        // بالمعلومات المرسلة من العميل
        Personne personne = (Personne)request.getSession().getAttribute("personne");
        personne.setDatenaissance(datenaissance);
        personne.setMarie(marie);
        personne.setNbenfants(nbenfants);
        personne.setNom(nom);
        personne.setPrenom(prenom);
        // الاستمرارية
        try {
            if (id == -1) {
                // إنشاء
                service.saveOne(personne);
            } else {
                // تحديث
                service.updateOne(personne);
            }
        } catch (DaoException ex) {
            // إعادة عرض النموذج مع رسالة الخطأ الذي حدث
            showFormulaire(request, response, ex.getMessage());
            // انتهى
            return;
        }
        // إعادة التوجيه إلى قائمة الأشخاص
        response.sendRedirect("list");
    }

    // عرض النموذج المملوء مسبقًا
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
        // يتم إعداد نموذج العرض [edit]
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("datenaissance", request.getParameter("datenaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbenfants", request.getParameter("nbenfants").trim());
        // عرض العرض [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • الأسطر 7-13: يتم استرداد المعلمة [prenom] من الطلب POST والتحقق من صحتها. إذا تبين أنه غير صحيح، يتم تهيئة العنصر [erreurPrenom] برسالة خطأ ووضعه في سمات الطلب.
  • الأسطر 15-21: يتم إجراء عملية مماثلة للمعلمة [nom]
  • الأسطر 23-30: يتم العمل بنفس الطريقة بالنسبة للمعلمة [datenaissance]
  • السطر 32: يتم استرداد المعلمة [marie]. لا يتم التحقق من صحتها لأنها تأتي مبدئيًا من قيمة زر اختيار. ومع ذلك، لا شيء يمنع برنامجًا من إنشاء [POST /.../do/validate] مصحوبًا بمعلمة [marie] وهمية. لذلك يجب أن نختبر صحة هذا المعامل. هنا، نعتمد على إدارة الاستثناءات لدينا التي تؤدي إلى عرض الصفحة [exception.jsp] إذا لم يقم المتحكم بإدارتها بنفسه. لذا، إذا فشل تحويل المعلمة [marie] إلى قيمة منطقية في السطر 32، فسيتم إصدار استثناء يؤدي إلى إرسال الصفحة [exception.jsp] إلى العميل. هذا الأداء يناسبنا.
  • الأسطر 34-50: يتم استرداد المعلمة [nbenfants] والتحقق من قيمتها.
  • السطر 52: يتم استرداد المعلمة [id] دون التحقق من قيمتها
  • الأسطر 54-59: إذا كان النموذج غير صحيح، يتم إعادة عرضه مع رسائل الخطأ التي تم إنشاؤها مسبقًا
  • الأسطر 62-67: إذا كان صالحًا، يتم إنشاء كائن جديد [Personne] باستخدام عناصر النموذج
  • السطور 69-82: يتم حفظ الشخص. قد يفشل الحفظ. في بيئة متعددة المستخدمين، قد يكون الشخص المراد تعديله قد تم حذفه أو تعديله بالفعل بواسطة شخص آخر. في هذه الحالة، ستطلق الطبقة [dao] استثناءً يتم التعامل معه هنا.
  • السطر 84: إذا لم تكن هناك استثناءات، يتم إعادة توجيه العميل إلى عنوان URL [/do/list] لعرض الحالة الجديدة للمجموعة عليه.
  • السطر 79: إذا حدثت استثناء أثناء الحفظ، نطلب إعادة عرض النموذج الأولي مع تمرير رسالة خطأ الاستثناء إليه (المعلمة الثالثة).

تقوم الطريقة [showFormulaire] (الأسطر 88-97) بإنشاء النموذج اللازم للصفحة [edit.jsp] باستخدام القيم التي تم إدخالها (request.getParameter(" ... ")). نتذكر أن رسائل الخطأ قد تم وضعها بالفعل في النموذج بواسطة الطريقة [doValidatePersonne]. يتم عرض الصفحة [edit.jsp] في السطر 99.

3.4.4. اختبارات تطبيق الويب

تم عرض عدد من الاختبارات في الفقرة 3.4.1. ندعو القارئ إلى إعادة تشغيلها. نعرض هنا لقطات شاشة أخرى توضح حالات تعارض الوصول إلى البيانات في بيئة متعددة المستخدمين:

[Firefox] سيكون متصفح المستخدم U1. يطلب هذا الأخير عنوان URL [http://localhost:8080/spring-jpa-hibernate-personnes-crud/do/list]:

Image

[IE7] سيكون متصفح المستخدم U2. هذا الأخير يطلب نفس عنوان URL:

Image

يقوم المستخدم U1 بتعديل بيانات الشخص [p2]:

Image

يقوم المستخدم U2 بنفس الشيء:

Image

يقوم المستخدم U1 بإجراء تعديلات والتحقق من صحتها:

المستخدم U2 يفعل الشيء نفسه:

يعود المستخدم U2 إلى قائمة الأشخاص باستخدام الرابط [Retour à la liste] الخاص بالنموذج:

Image

ويجد الشخص [Lemarchand] كما عدّله U1 (متزوج، ولديه طفلان). تغير رقم إصدار p2. الآن U2 يحذف [p2]:

لا يزال لدى U1 قائمته الخاصة ويريد تعديل [p2] مرة أخرى:

يستخدم U1 الرابط [Retour à la liste] لمعرفة ما يجري:

Image

يكتشف أن [p2] لم يعد بالفعل جزءًا من القائمة...

3.4.5. الإصدار 2

نقوم بتعديل الإصدار السابق قليلاً لاستخدام أرشيفات الطبقات [service, dao, jpa] بدلاً من أكوادها المصدرية:

  • في [1]: مشروع Eclipse الجديد. يُلاحظ اختفاء الحزم [service, dao, entites]. فقد تم تضمينها في الأرشيف [service-dao-jpa-personne.jar] [2] الموجود في [WEB-INF/lib].
  • يوجد ملف المشروع في [4]. سنقوم باستيراده.

لا يوجد شيء آخر يجب القيام به. عند تشغيل تطبيق الويب الجديد وطلب قائمة الأشخاص، يتم تلقي الرد التالي:

 

لا يجد Hibernate الكيان [Personne]. لحل هذه المشكلة، يتعين علينا الإعلان صراحةً في [persistence.xml] عن الكيانات المدارة:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.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_1_0.xsd">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <class>entites.Personne</class>
    </persistence-unit>
</persistence>
  • السطر 7: تم الإعلان عن الكيان Personne.

وبذلك، تختفي الاستثناء:

 

3.4.6. تغيير التنفيذ JPA

  • إلى [1]: المشروع الجديد Eclipse
  • إلى [2]: استبدلت مكتبات Toplink مكتبات Hibernate
  • مجلد المشروع موجود في [4]. سنقوم باستيراده.

تغيير التنفيذ JPA لا يتطلب سوى بعض التغييرات في الملف [spring-config.xml]. لا شيء آخر يتغير. تم شرح التغييرات التي تم إجراؤها في الملف [spring-config.xml] في الفقرة 3.1.9:


<?xml version="1.0" encoding="UTF-8"?>

<!-- يجب تشغيل JVM باستخدام الوسيطة -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à remplacer par le chemin exact de spring-agent.jar)-->

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
...    
            <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
...
    </bean>
...
</beans>

يجب تغيير بضعة أسطر للانتقال من Hibernate إلى Toplink:

  • السطر 11: يتم الآن تنفيذ JPA بواسطة Toplink
  • السطر 13: الخاصية [databasePlatform] لها قيمة مختلفة عن تلك الموجودة في Hibernate: اسم فئة خاصة بـ Toplink. تم شرح مكان العثور على هذا الاسم في الفقرة 2.1.15.2.

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

 

سنلاحظ هنا مشكلة تمت مواجهتها ووصفها في الفقرة 3.1.9. يتم حلها عن طريق تشغيل JVM باستخدام وكيل Spring. للقيام بذلك، نقوم بتعديل إعدادات تشغيل Tomcat:

  • إلى [1]: تم اختيار الخيار [Run / Run...] لتعديل تكوين Tomcat
  • إلى [2]: تم تحديد علامة التبويب [Arguments]
  • في [3]: تمت إضافة المعلمة -javaagent كما هو موضح في الفقرة 3.1.9.

بعد ذلك، يمكن طلب قائمة الأشخاص:

Image

3.5. أمثلة أخرى

كنا نرغب في عرض مثال على الويب يتم فيه استبدال حاوية Spring بحاوية Jboss Ejb3 التي تمت دراستها في الفقرة 3.2:

  • في [1]: مشروع Eclipse
  • إلى [3]: موقعه في مجلد الأمثلة. سنقوم باستيراده.

استخدمنا التكوين [jboss-config.xml, persistence.xml] الموصوف في الفقرة 3.2، ثم قمنا بتعديل الطريقة [init] في وحدة التحكم [Application.java] على النحو التالي:


// init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        try {
            // يتم استرداد معلمات التهيئة الخاصة بـ servlet
            ServletConfig config = getServletConfig();
            // معالجة معلمات التهيئة الأخرى
            String valeur = null;
            for (int i = 0; i < paramètres.length; i++) {
                // قيمة المعلمة
                valeur = config.getInitParameter(paramètres[i]);
                // هل المعلمة موجودة؟
                if (valeur == null) {
                    // تسجيل الخطأ
                    erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
                } else {
                    // يتم حفظ قيمة المعلمة
                    params.put(paramètres[i], valeur);
                }
            }
            // عنوان URL للطريقة [erreurs] له معالجة خاصة
            urlErreurs = config.getInitParameter("urlErreurs");
            if (urlErreurs == null)
                throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
            // تكوين التطبيق
            // يتم تشغيل الحاوية EJB3 JBoss
            // يتم استخدام ملفات التكوين ejb3-interceptors-aop.xml و embedded-jboss-beans.xml
            EJB3StandaloneBootstrap.boot(null);

            // إنشاء حبوب خاصة بالتطبيق
            EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

            // يتم نشر جميع ملفات EJB الموجودة في مسار فئات التطبيق
            //EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
            EJB3StandaloneBootstrap.scanClasspath();

            // يتم تهيئة السياق JNDI. يتم استخدام الملف jndi.properties
            InitialContext initialContext = new InitialContext();

            // إنشاء مثيل لطبقة الخدمة
            service = (IService) initialContext.lookup("Service/local");
            // يتم إفراغ قاعدة البيانات
            clean();
            // يتم ملؤها
            fill();
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }
  • الأسطر 28-38: يتم تشغيل الحاوية Ejb3. وهي تحل محل الحاوية Spring.
  • السطر 41: نطلب مرجعًا على طبقة [service] للتطبيق.

في البداية، هذه هي التعديلات الوحيدة التي يجب إجراؤها. عند التنفيذ، تظهر الرسالة التالية:

 

لم أتمكن من فهم أين تكمن المشكلة بالضبط. يبدو أن الاستثناء الذي أبلغ عنه Tomcat يشير إلى أن الكائن المسمى "TransactionManager" قد طُلب من الخدمة JNDI وأن هذه الخدمة لم تكن تعرفه. أترك للقراء مهمة إيجاد حل لهذه المشكلة. إذا تم العثور على حل، فسيتم تضمينه في الوثيقة.