Skip to content

3. JPA در یک معماری چندلایه

برای مطالعه API JPA، از معماری آزمایشی زیر استفاده کردیم:

برنامه‌های آزمایشی ما برنامه‌های کنسولی بودند که مستقیماً با لایه JPA تعامل داشتند. با این کار، ما روش‌های اصلی لایه JPA را بررسی کردیم. ما در یک محیط موسوم به «Java SE» (ویرایش استاندارد) کار می‌کردیم. JPA هم در محیط‌های Java SE و Java EE5 (ویرایش سازمانی) اجرا می‌شود.

اکنون که با پیکربندی پل رابطه‌ای/شیءگرا و استفاده از متدهای لایه JPA تا حدی آشنا شده‌ایم، به یک معماری چندلایه سنتی‌تر بازمی‌گردیم:

لایه [JPA] از طریق یک معماری دو لایه متشکل از [metier] و [dao] قابل دسترسی خواهد بود. چارچوب Spring [7]، و پس از آن کانtejner EJB3 از JBoss و [8]، برای پیوند دادن این لایه‌ها به یکدیگر استفاده خواهد شد.

قبلاً اشاره کردیم که JPA در محیط‌های SE و EE5 در دسترس است. محیط جاوا EE5 خدمات متعددی در ارتباط با دسترسی به داده‌های پایدار ارائه می‌دهد، از جمله استخرهای اتصال، مدیران تراکنش و غیره. ممکن است برای یک توسعه‌دهنده جالب باشد که از این خدمات استفاده کند. محیط جاوای EE5 هنوز به طور گسترده مورد استفاده قرار نگرفته است (مه ۲۰۰۷). این محیط در حال حاضر بر روی سرور برنامه‌های کاربردی Sun 9.x (GlassFish) در دسترس است. یک سرور برنامه در واقع یک سرور برنامه وب است. اگر کسی یک برنامه گرافیکی مستقل مانند یک برنامه Swing بسازد، نمی‌تواند به محیط EE و خدماتی که ارائه می‌دهد دسترسی پیدا کند. این یک مشکل است. ما در حال مشاهده محیط‌های «مستقل» EE و c.a.d هستیم. که می‌توان از آن‌ها خارج از یک سرور برنامه‌ای استفاده کرد. این وضعیت در مورد JBoss و EJB3 صدق می‌کند که در این سند از آن‌ها استفاده خواهیم کرد.

در محیط EE5، لایه‌ها توسط اشیایی به نام EJB (Enterprise Java Beans) پیاده‌سازی می‌شوند. در نسخه‌های قبلی EE، EJB (EJB و 2.x) پیاده‌سازی و تست آن‌ها دشوار تلقی می‌شد و گاهی عملکرد ضعیفی داشتند. تفاوتی بین «اِنتیتی» EJB2.x و «سشن» EJB2.x قائل می‌شوند. به طور خلاصه، یک «اِنتیتی» EJB2.x نمایانگر یک سطر در یک جدول پایگاه داده است، در حالی که یک «سِشن» EJB2.x ابجکتی است که برای پیاده‌سازی لایه‌های [metier] استفاده می‌شود، لایه‌های [dao] یک معماری چندلایه. یکی از انتقادات اصلی که به لایه‌هایی که با EJB پیاده‌سازی شده‌اند وارد می‌شود این است که آنها فقط می‌توانند در داخل کانتینرهای EJB استفاده شوند، سرویسی که توسط محیط EE ارائه می‌شود. این امر تست واحد را مشکل‌ساز می‌کند. بنابراین، در نمودار بالا، تست‌های واحد برای لایه‌های [metier] و [dao] که با استفاده از EJB ساخته شده‌اند، مستلزم استقرار یک سرور اپلیکیشن هستند – عملیاتی نسبتاً دست‌وپاگیر که چندان توسعه‌دهندگان را به انجام مکرر تست‌ها ترغیب نمی‌کند.

چارچوب Spring در پاسخ به پیچیدگی EJB2 ایجاد شد. در یک محیط SE، Spring تعداد قابل توجهی از خدماتی را که معمولاً توسط محیط‌های EE ارائه می‌شوند، فراهم می‌کند. بنابراین، در حوزه «پایداری داده‌ها»—که در اینجا مورد توجه ماست—اسپرینگ استخرهای اتصال و مدیران تراکنش مورد نیاز برنامه‌های کاربردی را فراهم می‌کند. ظهور اسپرینگ فرهنگ تست واحد را ترویج داده است، فرهنگی که ناگهان پیاده‌سازی آن بسیار آسان‌تر شده است. اسپرینگ امکان پیاده‌سازی لایه‌های یک برنامه را با استفاده از اشیاء استاندارد جاوا (POJO، Plain Old/Ordinary Java Object) فراهم می‌کند و این امکان را می‌دهد که این اشیاء در زمینه‌ای متفاوت دوباره مورد استفاده قرار گیرند. در نهایت، این فریم‌ورک ابزارهای شخص ثالث متعددی را به شکلی نسبتاً شفاف یکپارچه می‌کند، به‌ویژه ابزارهای پایداری داده‌ای مانند Hibernate، iBatis و غیره.

Java EE5 برای رفع کاستی‌های مشخصه‌ی قبلی EE طراحی شد. EJB و 2.x به EJB3 تبدیل شده‌اند. این‌ها نمونه‌های POJOs هستند که با حاشیه‌نویسی‌ها برچسب‌گذاری شده‌اند و وقتی در یک کانتینر EJB3 قرار می‌گیرند، به اشیاء ویژه‌ای تبدیل می‌شوند. در داخل این کانتینر، EJB3 قادر خواهد بود از خدمات کانتینر (استخر اتصال، مدیر تراکنش و غیره) بهره‌مند شود. خارج از کانتینر EJB3، EJB3 به یک شیء معمولی جاوا تبدیل می‌شود. انوتیشن‌های EJB آن نادیده گرفته می‌شوند.

در بالا، ما Spring و JBoss EJB3 را به عنوان یک چارچوب احتمالی برای معماری چندلایه خود به تصویر کشیدیم. این چارچوب است که خدمات مورد نیاز ما را فراهم می‌کند: یک استخر اتصال و یک مدیر تراکنش.

  • با Spring، لایه‌ها با استفاده از POJOs پیاده‌سازی خواهند شد. این لایه‌ها از طریق تزریق وابستگی به این POJOs به خدمات Spring (استخر اتصال، مدیر تراکنش) دسترسی خواهند داشت: هنگامی که این لایه‌ها نمونه‌سازی می‌شوند، Spring مراجعی به خدماتی را که نیاز خواهند داشت، تزریق می‌کند.
  • JBoss EJB3 یک کانتینر EJB است که قادر به اجرا خارج از سرور برنامه است. اصل کار آن (از دیدگاه توسعه‌دهنده) مشابه آنچه برای Spring توصیف شده است می‌باشد. تفاوت‌های اندکی وجود دارد.

این سند را با مثالی از یک برنامه وب سه‌لایه که ساده اما در عین حال نمونه‌ای است، به پایان می‌رسانیم:

3.1. مثال ۱: اسپرینگ / JPA با انتیت Person

ما انتیت «Personne» را که در بخش 2.1 مورد بحث قرار گرفت، در یک معماری چندلایه ادغام می‌کنیم که در آن لایه‌ها با استفاده از Spring یکپارچه شده و لایه پایداری (persistence) توسط Hibernate پیاده‌سازی می‌شود.

فرض بر این است که خواننده در اینجا درک پایه‌ای از Spring دارد. اگر این‌گونه نیست، سند زیر مفهوم تزریق وابستگی را که در قلب Spring قرار دارد، توضیح می‌دهد:

[ref3]: Spring IoC (معکوس‌سازی کنترل) [http://tahe.developpez.com/java/springioc].

3.1.1. پروژه Eclipse/Spring/Hibernate « »

پروژه اکلیپس به شرح زیر است:

  • در [1]: پروژه اکلیپس. این پروژه را می‌توان در [6] در میان مثال‌های آموزش [5] یافت. ما آن را وارد خواهیم کرد.
  • در [2]: کد جاوا برای لایه‌ها، سازمان‌یافته در بسته‌ها:
    • [entites]: پکیج entities JPA
    • [dao]: لایه دسترسی به داده – مبتنی بر لایه JPA
    • [service]: یک لایه سرویس به جای لایه کسب‌وکار. سرویس تراکنش کانtejner در اینجا استفاده خواهد شد.
    • [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 است که بیشتر کارهای دسترسی به داده‌ها را مدیریت می‌کند.
  • خط ۱۰: کلاس [Dao] رابط [IDao] را پیاده‌سازی می‌کند
  • خط ۱۳: شیئی از نوع [EntityManager]، که برای دستکاری زمینه پایداری JPA استفاده خواهد شد. برای سهولت، گاهی اوقات آن را خودِ زمینهٔ پایداری می‌نامیم. زمینهٔ پایداری شامل اِنتیته‌های Personne خواهد بود.
  • خط ۱۲: در هیچ نقطه‌ای از کد فیلد [EntityManager em] مقداردهی اولیه نشده است. این فیلد توسط Spring هنگام شروع برنامه مقداردهی خواهد شد. این anotation JPA @PersistenceContext در خط ۱۲ است که به Spring دستور می‌دهد یک مدیر زمینه پایداری را به em تزریق کند.
  • خطوط ۲۶–۲۸: فهرست همه افراد از طریق پرس‌وجوی JPQL بازیابی می‌شود.
  • خطوط ۳۲–۳۵: فهرست تمام افرادی که نامشان با الگوی خاصی مطابقت دارد از طریق پرس‌وجوی JPQL بازیابی می‌شود.
  • خطوط ۳۸–۴۰: شخص با شناسه‌ی داده‌شده با استفاده از متد `find` از `API JPA` بازیابی می‌شود. اگر شخص وجود نداشته باشد، یک نشانگر `null` بازمی‌گرداند.
  • خطوط ۴۳–۴۶: یک شخص توسط متد `persist` کلاس‌های `API` و `JPA` پایدارسازی می‌شود. این متد شخص را پایدار می‌سازد.
  • خطوط ۴۹–۵۱: یک شخص با استفاده از متد `merge` کلاس `API JPA` به‌روزرسانی می‌شود. این متد تنها در صورتی معنادار است که شخص در حال به‌روزرسانی قبلاً جدا شده باشد. این متد شخص ایجاد شده به این روش را پایدار می‌سازد.
  • خطوط 16–22: حذف فردی که شناسهٔ آن به‌عنوان پارامتر ارسال می‌شود، در دو مرحله انجام می‌شود:
    • خط ۱۷: شخص در زمینه پایداری جستجو می‌شود
    • خطوط ۱۸–۲۰: اگر شخص پیدا نشود، یک استثنا با کد خطای ۲ پرتاب می‌شود
    • خط ۲۱: اگر شخص پیدا شود، با استفاده از متد `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;
    }

    // گیرنده و تنظیم‌کننده

    public int getCode() {
        return code;
    }

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

}
  • خط ۴: [DaoException] از [RuntimeException] مشتق شده است. بنابراین این یک نوع استثنا است که کامپایلر از ما نمی‌خواهد آن را با بلوک try/catch مدیریت کنیم یا در امضای متد وارد کنیم. به همین دلیل، [DaoException] در امضای متد [deleteOne] در رابط [IDao] گنجانده نمی‌شود. این امکان را فراهم می‌کند که این رابط توسط کلاسی پیاده‌سازی شود که نوع دیگری از استثنا را پرتاب می‌کند، به شرطی که آن کلاس نیز از [RuntimeException] ارث ببرد.
  • برای تمایز بین خطاهایی که ممکن است رخ دهند، از کد خطا در خط ۷ استفاده می‌شود. سه سازنده در خطوط ۱۴، ۱۹ و ۲۴ متعلق به کلاس والد [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);

}
  • خطوط ۸–۲۴: رابط [IService] متدهای رابط [IDao] را در خود جای می‌دهد
  • خط ۲۷: متد [deleteArray] امکان حذف یک مجموعه از افراد را در یک تراکنش فراهم می‌کند: یا همه افراد حذف می‌شوند یا هیچ‌کدام.
  • خطوط ۳۰ و ۳۳: متدهایی مشابه [deleteArray] برای ذخیره (خط ۳۰) یا به‌روزرسانی (خط ۳۳) یک مجموعه از افراد در یک تراکنش.

پیاده‌سازی [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);
    }

}
  • خط ۶: آناوتیشن Spring با نام @Transactional نشان می‌دهد که تمام متدهای این کلاس باید در داخل یک تراکنش اجرا شوند. یک تراکنش قبل از شروع اجرای متد آغاز شده و پس از آن بسته می‌شود. اگر در حین اجرای متد، استثنایی از نوع [RuntimeException] یا یکی از انواع مشتق شده آن رخ دهد، یک رول‌بک (rollback) خودکار کل تراکنش را لغو می‌کند؛ در غیر این صورت، یک کامیت (commit) خودکار آن را تأیید می‌کند. شایان ذکر است که کد جاوا نیازی ندارد به تراکنش‌ها بپردازد. این‌ها توسط اسپرینگ مدیریت می‌شوند.
  • خط ۱۰: مرجعی به لایه [dao]. بعداً خواهیم دید که این مرجع هنگام شروع برنامه توسط Spring مقداردهی اولیه می‌شود.
  • متدهای [Service] به سادگی از خط ۱۰، متدهای رابط [IDao dao] را فراخوانی می‌کنند. بررسی کد را به خواننده واگذار می‌کنیم. دشواری خاصی وجود ندارد.
  • قبلاً اشاره کردیم که هر متد در [Service] در داخل یک تراکنش اجرا می‌شود. این تراکنش به نخ اجرای متد متصل می‌شود. در داخل این نخ، متدهای لایه [dao] اجرا می‌شوند. این متدها به طور خودکار به تراکنش نخ اجرا متصل خواهند شد. برای مثال، متد [deleteArray] (خط ۲۱) باید متد [deleteOne] را از لایه [dao] به تعداد N بار اجرا کند. این N اجرا در داخل نخ اجرایی متد [deleteArray] و در نتیجه در همان تراکنش انجام خواهد شد. در نتیجه، اگر همه چیز به خوبی پیش برود، همه آن‌ها commit (ثبت) خواهند شد، یا اگر در هر یک از N اجرای متد [deleteOne] در لایه [dao] یک استثنا رخ دهد، همه آن‌ها rollback (اباطل) خواهند شد.

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>
  • خط ۴: این فایل یک واحد پایداری به نام 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>
  • خطوط ۲–۵: تگ ریشه <beans> در فایل پیکربندی. ما در مورد ویژگی‌های مختلف این تگ توضیحی نمی‌دهیم. دقت کنید که آن‌ها را به درستی کپی و پیست کنید، زیرا اشتباه در هر یک از این ویژگی‌ها می‌تواند باعث خطاهایی شود که گاهی اوقات درک آن‌ها دشوار است.
  • خط ۸: بیان «dao» مرجعی به یک نمونه از کلاس [dao.Dao] است. یک نمونه واحد (singleton) ایجاد خواهد شد و لایه [dao] برنامه را پیاده‌سازی می‌کند.
  • خطوط ۹–۱۱: نمونه‌سازی لایه [service]. بیان «service» مرجعی به یک نمونه از کلاس [service.Service] است. یک نمونه واحد (singleton) ایجاد خواهد شد و لایه [service] برنامه را پیاده‌سازی خواهد کرد. ما دیده‌ایم که کلاس [service.Service] دارای یک فیلد خصوصی به نام [IDao dao] است. این فیلد در خط ۱۰ توسط بین «dao» که در خط ۸ تعریف شده است، مقداردهی اولیه می‌شود.
  • در نهایت، خطوط ۸ تا ۱۱ لایه‌های [dao] و [service] را پیکربندی کرده‌اند. بعداً خواهیم دید که چه زمانی و چگونه این اشیاء ایجاد خواهند شد.
  • خطوط ۳۵–۴۲: یک منبع داده تعریف شده است. ما قبلاً هنگام مطالعهٔ اشیاء JPA با Hibernate با مفهوم منبع داده آشنا شده‌ایم:

در مثال بالا، [c3p0] که به آن «استخر اتصال» گفته می‌شود، می‌توانست «منبع داده» نامیده شود. یک منبع داده سرویس «استخر اتصال» را فراهم می‌کند. با Spring، ما از یک منبع داده غیر از [c3p0] استفاده خواهیم کرد. این [DBCP] از پروژه Apache Commons DBCP [http://jakarta.apache.org/commons/dbcp/] است. آرشیوهای [DBCP] در کتابخانهٔ کاربری [jpa-spring] قرار داده شده‌اند:

 
  • خطوط ۳۸–۴۱: برای برقراری ارتباط با پایگاه داده مقصد، منبع داده باید درایور JDBC مورد استفاده (خط ۳۸)، URL پایگاه داده (خط ۳۹)، نام کاربری و رمز عبور اتصال (خطوط ۴۰–۴۱) را بشناسد.
  • خطوط ۱۴–۳۲: پیکربندی لایه JPA
  • خطوط 14–15: یک بیون از نوع [EntityManagerFactory] را تعریف می‌کند که قادر به ایجاد اشیاء از نوع [EntityManager] برای مدیریت زمینه‌های پایداری است. کلاس نمونه‌سازی‌شده [LocalContainerEntityManagerFactoryBean] توسط Spring فراهم می‌شود. این کلاس برای نمونه‌سازی به تعدادی پارامتر نیاز دارد که در خطوط 16–31 تعریف شده‌اند.
  • خط 16: منبع داده‌ای که برای به‌دست‌آوردن اتصالات به SGBD استفاده می‌شود. این همان منبع داده [DBCP] است که در خطوط 35–42 تعریف شده است.
  • خطوط ۱۷–۲۷: پیاده‌سازی JPA که باید استفاده شود
  • خطوط ۱۸–۲۶: Hibernate (خط ۱۹) را به‌عنوان پیاده‌سازی JPA که باید استفاده شود، تعریف می‌کنند
  • خطوط ۲۳–۲۴: گویش SQL که Hibernate باید با هدف SGBD، در این مورد MySQL5، استفاده کند.
  • خط 25: مشخص می‌کند که پایگاه داده باید هنگام شروع برنامه ایجاد (drop و create) شود.
  • خطوط ۲۸–۳۱: یک «بارگذار کلاس» را تعریف می‌کنند. نمی‌توانم نقش این بین را به‌وضوح توضیح دهم، که توسط EntityManagerFactory در لایه JPA استفاده می‌شود. با این حال، این شامل ارسال نام یک آرشیو به JVM—که برنامه را اجرا می‌کند—است، آرشیویی که محتوای آن هنگام شروع برنامه، بارگذاری کلاس‌ها را مدیریت خواهد کرد. در اینجا، این آرشیو [spring-agent.jar] است که در کتابخانه کاربری [jpa-spring] قرار دارد (به بالا مراجعه کنید). خواهیم دید که Hibernate به این عامل نیاز ندارد، اما Toplink به آن نیاز دارد.
  • خطوط ۴۵–۵۰: مدیر تراکنش مورد استفاده را تعریف می‌کنند
  • خط ۴۵: نشان می‌دهد که تراکنش‌ها با استفاده از anotationهای جاوا مدیریت می‌شوند (می‌توانستند در spring-config.xml نیز اعلام شده باشند). به‌طور مشخص، این به anotation @Transactional اشاره دارد که در کلاس [Service] (خط ۶) یافت می‌شود.
  • خطوط ۴۶–۵۰: مدیر تراکنش
  • خط ۴۷: مدیر تراکنش کلاسی است که توسط Spring ارائه می‌شود
  • خطوط ۴۸–۴۹: مدیر تراکنش Spring باید از کلاس EntityManagerFactory که لایه JPA را مدیریت می‌کند، مطلع باشد. این همان کلاسی است که در خطوط ۱۴–۳۲ تعریف شده است.
  • خطوط ۵۷–۵۸: کلاسی را تعریف می‌کنند که انوتیشن‌های پایداری Spring را که در کد جاوا یافت می‌شوند، مدیریت می‌کند؛ مانند انوتیشن @PersistenceContext روی کلاس [dao.Dao] (خط ۱۲).
  • خطوط ۵۳–۵۴: کلاس اسپرینگ را تعریف می‌کند که به‌ویژه مدیریت anotation @Repository را بر عهده دارد، که یک کلاس دارای این anotation را واجد شرایط ترجمه استثناهای بومی از درایور JDBC SGBD به استثناهای عمومی اسپرینگ از نوع [DataAccessException] می‌سازد. این تبدیل، استثنای بومی JDBC را در درون یک نوع [DataAccessException] جای می‌دهد که زیرکلاس‌های مختلفی دارد:

Image

این ترجمه به برنامه کلاینت اجازه می‌دهد تا استثناءها را به صورت کلی، صرف‌نظر از هدف SGBD، مدیریت کند. ما از anotation @Repository در کد جاوا خود استفاده نکرده‌ایم. بنابراین، خطوط 53–54 غیرضروری هستند. ما آن‌ها را صرفاً برای اهداف اطلاع‌رسانی باقی گذاشته‌ایم.

ما اکنون کار با فایل پیکربندی Spring را به پایان رسانده‌ایم. این فایل پیچیده است و بسیاری از جنبه‌های آن نامشخص باقی مانده‌اند. این فایل از مستندات Spring گرفته شده است. خوشبختانه، تطبیق آن با شرایط مختلف اغلب به دو تغییر خلاصه می‌شود:

  • پایگاه داده هدف: خطوط ۳۸–۴۱. ما یک مثال اوریکل ارائه خواهیم داد.
  • پیاده‌سازی 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());
        }
    }
}
  • خط ۱۲: فایل [spring-config.xml] برای ایجاد یک شیء [ApplicationContext ctx] استفاده می‌شود که تصویری از حافظه این فایل است. بین‌هایی که در [spring-config.xml] تعریف شده‌اند در این نقطه نمونه‌سازی می‌شوند.
  • خط ۱۴: از کانکست برنامه ctx درخواست می‌شود تا مرجعی به لایه [service] ارائه دهد. می‌دانیم که این توسط یک بین به نام «service» نمایش داده می‌شود.
  • خط ۱۶: پایگاه داده با استفاده از متد clean خالی می‌شود. خطوط ۴۱–۴۵:
    • خطوط ۴۲–۴۴: فهرست همه افراد از زمینه پایداری درخواست می‌شود و یک حلقه روی آن‌ها اجرا می‌شود تا یکی‌یکی حذف شوند. شما ممکن است به یاد داشته باشید که [spring-config.xml] مشخص می‌کند که پایگاه داده باید هنگام شروع برنامه ایجاد شود. بنابراین، در مورد ما، فراخوانی متد clean غیرضروری است زیرا ما با یک پایگاه داده خالی شروع می‌کنیم.
  • خط ۱۸: متد fill پایگاه داده را پر می‌کند. این در خطوط ۳۲–۳۸ تعریف شده است:
    • خطوط ۳۴–۳۵: دو نفر ایجاد می‌شوند
    • خط ۳۷: به لایه [service] دستور داده می‌شود که آن‌ها را ماندگار کند.
  • خط ۲۰: متد dumpPersonnes افراد پایدار را نمایش می‌دهد. این متد در خطوط ۲۴–۲۹ تعریف شده است
    • خطوط ۲۶–۲۸: فهرست تمام افراد پایدار از لایه [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() {
...
    }
...
}
  • خط ۹: تذکر @BeforeClass متدی را مشخص می‌کند که برای راه‌اندازی پیکربندی مورد نیاز برای تست‌ها اجرا می‌شود. این متد قبل از اجرای اولین تست اجرا می‌شود. توضیح @AfterClass، که در اینجا استفاده نشده است، متدی را مشخص می‌کند که پس از اجرای تمام تست‌ها باید اجرا شود.
  • خطوط ۱۰–۱۷: متد `init` که با `@BeforeClass` نشانه‌گذاری شده است، از فایل پیکربندی Spring برای ایجاد نمونه‌های لایه‌های مختلف برنامه و به دست آوردن مرجعی به لایه `[service]` استفاده می‌کند. سپس تمام تست‌ها از این مرجع استفاده می‌کنند.
  • خط ۱۹: آناوتیشن @BeforeMethod متدی را مشخص می‌کند که قبل از هر تست اجرا شود. آناوتیشن @AfterMethod که در اینجا استفاده نشده است، متدی را مشخص می‌کند که بعد از هر تست اجرا شود.
  • خطوط ۲۰–۲۵: متد setUp که با @BeforeMethod نشانه‌گذاری شده است، پایگاه داده را خالی می‌کند (clean خطوط 52–56) و سپس آن را با دو نفر پر می‌کند (fill خطوط 42–49).
  • خط ۵۹: تگ @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());
    }
  • خطوط ۲–۸: تست ۰۱. باید توجه داشت که در ابتدای هر تست، پایگاه داده شامل دو نفر به نام‌های p1 و p2 به‌ترتیب است.
  • خط ۶: ما فهرست افراد را درخواست می‌کنیم
  • خط ۷: بررسی می‌کنیم که تعداد افراد در فهرست به‌دست‌آمده ۲ است
  • خط 14: ما فهرست افرادی را درخواست می‌کنیم که نام خانوادگی‌شان با p1 شروع می‌شود
  • ما بررسی می‌کنیم که لیست بازگردانده‌شده تنها شامل یک عنصر است (خط 15) و اینکه نام اولین شخص بازگردانده‌شده «Paul» است (خط 17)
  • خط 24: ما یک شخص به نام p3 ایجاد می‌کنیم
  • خط 25: آن را حفظ می‌کنیم
  • خط ۲۸: آن را دوباره از زمینه پایداری بازیابی می‌کنیم برای تأیید
  • خط ۳۲: بررسی می‌کنیم که شخص بازیابی‌شده واقعاً نام 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();

    }
  • خط ۵: ما شخص p1 را بازیابی می‌کنیم
  • خط ۱۰: بررسی نام آنها
  • خط ۱۱: شماره نسخهٔ آن‌ها را یادداشت کنید
  • خط ۱۳: ما نام اول آنها را تغییر می‌دهیم
  • خط ۱۵: تغییر ذخیره می‌شود
  • خط ۱۷: دوباره شخص p1 را بازیابی کنید
  • خط ۲۱: بررسی کنید که شماره نسخهٔ آن ۱ واحد افزایش یافته است

@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();
    }
  • خط ۵: درخواست شخص p2
  • خط ۱۰: بررسی نام آنها
  • خط ۱۲: آنها را حذف کنید
  • خط ۱۴: دوباره از شخص پرس‌وجو می‌کنیم
  • خط ۱۶: ما بررسی می‌کنیم که آن‌ها پیدا نشده‌اند

@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();
    }
  • خط ۵: یک آرایه از سه نفر ایجاد کنید که دو نفر از آن‌ها نام یکسانی «p4» دارند. این نقض قانون یکتایی نام @Entity Personne است:

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
  • خط ۱۱: آرایه سه نفره در زمینه پایداری (persistence context) قرار می‌گیرد. افزودن شخص دوم، p4، باید با شکست مواجه شود. از آنجایی که متد [saveArray] در داخل یک تراکنش اجرا می‌شود، هرگونه درج داده‌ای که ممکن است قبلاً انجام شده باشد، برگشت داده خواهد شد. در نهایت، هیچ رکورد جدیدی اضافه نخواهد شد.
  • خط ۱۸: بررسی می‌کنیم که [saveArray] واقعاً یک استثنا پرتاب کرده است
  • خطوط ۲۰–۲۱: بررسی می‌کنیم که شخص 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);
    }
  • خط ۶: شخص p1 را بازیابی می‌کنیم
  • خط ۱۲: تعداد فرزندان را ۱ افزایش می‌دهیم
  • خط ۱۴: شخص p1 در زمینه پایداری به‌روزرسانی می‌شود. متد [updateOne] نسخه جدید newp1 را از p1 پایدار می‌سازد. این نسخه در شمارهٔ ورژن خود با p1 تفاوت دارد که باید افزایش یافته باشد.
  • خط ۱۵: تعداد فرزندان برای `newp1` بررسی می‌شود.
  • خط ۲۱: ما یک به‌روزرسانی برای شخص p1 بر اساس نسخه قدیمی p1 درخواست می‌کنیم. باید یک استثنا پرتاب شود زیرا p1 جدیدترین نسخه از شخص p1 نیست. جدیدترین نسخه newp1 است.
  • خط ۲۳: بررسی می‌کنیم که خطا واقعاً رخ داده است
  • خطوط ۲۷–۳۵: بررسی می‌کنیم که اگر به‌روزرسانی از آخرین نسخه، `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);
        // ما دو تغییر را ذخیره می‌کنیم که دومی باید با شکست مواجه شود (شخص به‌درستی inizialize نشده است)
        // به دلیل تراکنش، هر دو باید سپس برگشت داده شوند
        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();
    }
  • آزمون ۸ مشابه آزمون ۶ است: این آزمون 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();
    }
  • آزمون ۹ مشابه آزمون قبلی است: این آزمون 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;
    }
  • هدف تست ۱۰ راه‌اندازی N نخ (خط ۹) برای افزایش موازی تعداد فرزندان یک شخص است. می‌خواهیم تأیید کنیم که سیستم شماره‌گذاری نسخه واقعاً می‌تواند از عهده این سناریو برآید. این سیستم برای همین منظور طراحی شده است.
  • خطوط ۵–۶: یک شخص به نام p3 ایجاد و سپس پایدارسازی می‌شود. او در ابتدا ۰ فرزند دارد.
  • خط ۷: شناسه‌ی آن‌ها ثبت می‌شود.
  • خطوط ۹–۱۴: N نخ به‌طور موازی راه‌اندازی می‌شوند که همگی مأمور افزایش تعداد فرزندان p3 به مقدار ۱ هستند.
  • خطوط ۱۶–۱۸: منتظر پایان تمام رشته‌ها می‌شویم.
  • خط ۲۰: ما درخواست مشاهدهٔ شخص p3 را داریم
  • خط ۲۲: بررسی می‌کنیم که آن‌ها اکنون 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é");
        // حلقه تا زمانی که با موفقیت یک واحد افزایش دهیم
        // تعداد فرزندان شخص 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());
            // تعداد فرزندان شخص را ۱ افزایش می‌دهد
            personne.setNbenfants(nbEnfants + 1);
            // به مدت ۱۰ میلی‌ثانیه منتظر می‌ماند قبل از واگذاری پردازنده
            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);
    }
}
  • خطوط ۱۵–۱۹: سازنده اطلاعات مورد نیاز برای کارکرد خود را ذخیره می‌کند: نام آن (خط ۱۶)، مرجع به لایه [service] که باید استفاده کند (خط ۱۷) و شناسه‌ی شخص p که تعداد فرزندانش را باید افزایش دهد (خط ۱۸).
  • خطوط ۲۲–۶۶: متد [run] به‌طور موازی توسط همه نخ‌ها اجرا می‌شود.
  • خط ۲۹: نخ بارها تلاش می‌کند تعداد فرزندان شخص p را افزایش دهد. تنها پس از موفقیت متوقف می‌شود.
  • خط ۳۱: از شخص p پرس‌وجو می‌شود
  • خط ۳۶: تعداد فرزندان آنها در حافظه افزایش می‌یابد
  • خطوط ۳۸–۴۷: یک وقفه ۱۰ میلی‌ثانیه‌ای وجود دارد. این کار به سایر نخ‌ها اجازه می‌دهد تا همان نسخه از شخص p را به دست آورند. در نتیجه، همزمان، چندین نخ همان نسخه از شخص p را در اختیار خواهند داشت و مایل به تغییر آن خواهند بود. این رفتار مورد نظر است.
  • خط ۵۲: پس از پایان وقفه، نخ از لایه [service] می‌خواهد که تغییر را پایدار کند. ما می‌دانیم که به مرور زمان خطاها (exceptions) رخ خواهند داد، بنابراین این عملیات را در یک بلوک try/catch قرار داده‌ایم.
  • خط ۵۵: آزمایش‌ها نشان می‌دهند که استثناهایی از نوع [javax.persistence.OptimisticLockException] پرتاب می‌شوند. این طبیعی است: این استثنایی است که توسط لایه JPA پرتاب می‌شود، زمانی که یک نخ (thread) سعی می‌کند شخص p را بدون داشتن آخرین نسخه از آن شخص، تغییر دهد. این استثنا نادیده گرفته می‌شود تا به نخ اجازه دهد عملیات را تا زمان موفقیت دوباره امتحان کند.
  • خط ۵۷: تست‌ها نشان می‌دهند که ما همچنین با استثناهایی از نوع [org.springframework.transaction.UnexpectedRollbackException] مواجه می‌شویم. این موضوع آزاردهنده و غیرمنتظره است. من هیچ توضیحی برای آن ندارم. ما اکنون به اسپرینگ وابسته هستیم، هرچند ترجیح می‌دادیم از این کار اجتناب کنیم. این بدان معناست که اگر برای مثال، اپلیکیشن خود را در JBoss Ejb3 اجرا کنیم، کد تِرد باید تغییر کند. خطای اسپرینگ در اینجا نیز نادیده گرفته می‌شود تا به تِرد اجازه دهد عملیات افزایش را مجدداً امتحان کند.
  • خط ۵۹: انواع دیگر استثناها به برنامه بازگردانده می‌شوند.

وقتی [TestNG] اجرا می‌شود، نتایج زیر به دست می‌آیند:

Image

تمام ۱۰ تست با موفقیت پاس شدند.

آزمون ۱۰ نیاز به توضیح بیشتری دارد زیرا موفقیت آن تا حدی مرموز به نظر می‌رسد. ابتدا بیایید به پیکربندی لایه [dao] بازگردیم:


public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

  • خط ۴: یک شیء [EntityManager] از طریق تگ @PersistenceContext به فیلد 'em' تزریق می‌شود. لایه [dao] تنها یک بار نمونه‌سازی می‌شود. این یک شیء واحد (singleton) است که توسط تمام نخ‌هایی که از لایه JPA استفاده می‌کنند، به کار گرفته می‌شود. در نتیجه، `em` مربوط به EntityManager توسط همه نخ‌ها مشترک است. این موضوع را می‌توان با نمایش مقدار `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());
            // تعداد فرزندان شخص را ۱ افزایش می‌دهد
            personne.setNbenfants(nbEnfants + 1);
            // به مدت ۱۰ میلی‌ثانیه منتظر می‌ماند و سپس پردازنده را واگذار می‌کند
            try {
                //پیگیری
                suivi("début attente");
                // وقفه برای واگذاری پردازنده
                Thread.sleep(10);
                //پیگیری
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
}
  • خط ۳: یک نخ T1 شخص p را بازیابی می‌کند
  • خط ۸: تعداد فرزندان p را افزایش می‌دهد
  • خط ۱۴: نخ T1 متوقف می‌شود

یک نخ T2 کنترل را به دست می‌گیرد و همچنین خط ۳ را اجرا می‌کند: این نخ همان شخص p را که در T1 درخواست شده بود، درخواست می‌کند. اگر زمینه پایداری برای نخ‌ها یکسان بود، شخص p – که به لطف T1 در زمینه قرار دارد – باید به T2 بازگردانده شود. این به این دلیل است که متد [getOne] متد [EntityManager] را فراخوانی می‌کند.API، JPA را فراخوانی می‌کند و این متد تنها در صورتی به پایگاه داده دسترسی پیدا می‌کند که شیء درخواست‌شده بخشی از زمینه پایداری نباشد؛ در غیر این صورت، شیء را از زمینه پایداری بازمی‌گرداند. اگر چنین بود، T1 و T2 همان شخص p را نگه می‌داشتند. سپس T2 بار دیگر تعداد فرزندان p را ۱ افزایش می‌داد (خط ۸). اگر یکی از رشته‌ها پس از مکث، مقدار را با موفقیت به‌روزرسانی کند، تعداد فرزندان p به جای افزایش ۱، به میزان ۲ افزایش خواهد یافت. در این صورت ممکن است انتظار رود که N رشته، تعداد فرزندان را نه روی N، بلکه روی مقدار بالاتری تنظیم کنند. با این حال، چنین نیست. بنابراین می‌توان نتیجه گرفت که T1 و T2 به یک مرجع یکسان برای p اشاره نمی‌کنند. ما این موضوع را با نمایش دادن آدرس p توسط نخ‌ها تأیید می‌کنیم: این آدرس برای هر یک از آن‌ها متفاوت است.

بنابراین به نظر می‌رسد که رشته‌ها:

  • مدیر یکسانِ زمینهٔ پایداری (EntityManager) را به اشتراک می‌گذارند
  • اما هر کدام زمینه پایداری (persistence context) خود را دارند.

این‌ها صرفاً فرضیاتی هستند و نظر یک کارشناس در اینجا مفید خواهد بود.

3.1.8. تغییر از SGBD

برای تغییر از SGBD، فایل [src/spring-config.xml] [2] را با فایل [spring-config.xml] از پوشه SGBD [conf] QZXW2HTML جایگزین کنید.P003162ZQX.

فایل Oracle [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="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 استفاده شده بود، تغییر کرده‌اند:

  • خط ۱۴: گویش SQL که Hibernate باید استفاده کند
  • خطوط ۲۵–۲۸: ویژگی‌های اتصال JDBC با SGBD

از خوانندگان دعوت می‌شود تا آزمایش‌های توصیف‌شده برای MySQL5 را با سایر فایل‌های SGBD تکرار کنند.

3.1.9. تغییر پیاده‌سازی JPA

بیایید به معماری آزمون‌های قبلی بازگردیم:

ما پیاده‌سازی JPA / Hibernate را با پیاده‌سازی JPA / Toplink جایگزین می‌کنیم. از آنجا که Toplink از همان کتابخانه‌های Hibernate استفاده نمی‌کند، از یک پروژه جدید Eclipse استفاده می‌کنیم:

  • در [1]: پروژه اکلیپس. این پروژه با نسخه قبلی یکسان است. تنها تغییرات مربوط به فایل پیکربندی [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 تنها چند خط نیاز به تغییر دارند:

  • خط ۱۹: پیاده‌سازی JPA اکنون توسط Toplink مدیریت می‌شود
  • خط ۲۳: ویژگی [databasePlatform] مقدار متفاوتی نسبت به آنچه در Hibernate استفاده می‌شود دارد: نام کلاسی که مختص Toplink است. محل یافتن این نام در بخش 2.1.15.2 توضیح داده شده است.

همین. توجه کنید که با استفاده از Spring، چقدر آسان است که بین پیاده‌سازی SGBD و JPA جابجا شوید.

با این حال، ما هنوز کاملاً تمام نکرده‌ایم. برای مثال، وقتی [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 

پیام خطا در خط ۱ شما را به مراجعه به مستندات 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>

خط اول استثنا به کلاسی به نام [InstrumentationLoadTimeWeaver] اشاره دارد که در خط ۱۳ فایل پیکربندی Spring یافت می‌شود. مستندات Spring توضیح می‌دهد که این کلاس در برخی موارد برای بارگذاری کلاس‌های برنامه ضروری است و برای عملکرد آن، JVM باید با یک عامل (agent) راه‌اندازی شود. این عامل توسط Spring ارائه می‌شود و [spring-agent] نام دارد:

  • فایل [spring-agent.jar] در پوشه <examples>/lib قرار دارد. این فایل همراه با توزیع Spring ارائه می‌شود (به بخش 5.11 مراجعه کنید).
  • در [3]، یک پیکربندی زمان اجرا [Run/Run...] ایجاد می‌شود
  • در [4]، یک پیکربندی زمان اجرای جاوا ایجاد کنید (انواع مختلفی از پیکربندی‌های زمان اجرا وجود دارد)
  • در [5]، زبانه [Main] را انتخاب کنید
  • در [6]، برای پیکربندی یک نام وارد کنید
  • در [7]، پروژه Eclipse مرتبط با این پیکربندی را نام‌گذاری کنید (از دکمه Browse استفاده کنید)
  • در [8]، نام کلاس Java حاوی متد [main] را وارد کنید (از دکمه مرور استفاده کنید)
  • در [9]، به برگه [Arguments] بروید. در اینجا می‌توانید دو نوع آرگومان را مشخص کنید:
    • در [9]، آنهایی که به متد [main] پاس داده می‌شوند
    • در [10]، آنهایی که به JVM ارسال می‌شوند و کد را اجرا خواهند کرد. ایجنت Spring با استفاده از پارامتر -javaagent:value در JVM تعریف می‌شود. مقدار، مسیر فایل [spring-agent.jar] است.
  • در [11]: پیکربندی اعتبارسنجی می‌شود
  • در [12]: پیکربندی ایجاد می‌شود
  • در [13]: اجرا می‌شود

پس از انجام این کار، [InitDB] اجرا می‌شود و همان نتایج را مانند Hibernate تولید می‌کند. برای [TestNG]، به همان روش ادامه دهید:

  • در [1]، یک پیکربندی اجرا به نام [Run/Run...] ایجاد کنید
  • برای [2]، یک پیکربندی اجرا به نام TestNG ایجاد کنید
  • در [3]، زبانه [Test] را انتخاب کنید
  • در [4]، برای پیکربندی یک نام انتخاب کنید
  • در [5]، نام پروژه Eclipse مرتبط با این پیکربندی را مشخص کنید (از دکمه مرور استفاده کنید)
  • در [6]، کلاس تست را نام‌گذاری کنید (از دکمه مرور استفاده کنید)
  • در [7]، به برگه [Arguments] بروید.
  • در [8]: آرگومان -javaagent را برای JVM تنظیم کنید.
  • در [9]: پیکربندی را تأیید کنید
  • در [10]: پیکربندی ایجاد می‌شود
  • در [11]: اجرا می‌شود

پس از انجام این کار، [TestNG] اجرا می‌شود و نتایج مشابهی با Hibernate تولید می‌کند.

3.2. مثال ۲: JBoss EJB3 / JPA با انتیت Person

ما از همان مثال قبلی استفاده می‌کنیم، اما آن را در یک کانتینر EJB3 اجرا می‌کنیم که مخصوص JBoss است:

یک کانtejner EJB3 معمولاً در یک سرور برنامه یکپارچه می‌شود. JBoss یک کانtejner EJB3 «مستقل» (standalone) ارائه می‌دهد که می‌توان از آن خارج از یک سرور برنامه استفاده کرد. خواهیم دید که این کانtejner خدماتی مشابه با خدمات ارائه‌شده توسط Spring ارائه می‌دهد. تلاش خواهیم کرد تا مشخص کنیم کدام یک از این کانtejnerها کاربردی‌تر است.

نصب کانتینر EJB3 JBoss در بخش 5.12 توضیح داده شده است.

3.2.1. پروژه Eclipse / JBoss EJB3 / Hibernate

پروژه اکلیپس به شرح زیر است:

  • در [1]: پروژهٔ Eclipse. این پروژه را می‌توان در [6] در میان مثال‌های آموزش [5] یافت. ما آن را وارد خواهیم کرد.
  • در [2]: کد جاوا برای لایه‌ها، سازمان‌یافته در بسته‌ها:
    • [entites]: پکیج entities JPA
    • [dao]: لایه دسترسی به داده – مبتنی بر لایه JPA
    • [service]: یک لایه سرویس به جای لایه کسب‌وکار. سرویس تراکنش کانtejner 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 استفاده می‌شود یکسان است. تنها anotationهای جاوا تغییر کرده‌اند و این‌ها همان‌هایی هستند که در حال بحث در موردشان هستیم.
  • خط ۴: تگ @Stateless کلاس [Dao] را به یک EJB بدون حالت تبدیل می‌کند. تگ @Stateful یک کلاس را به یک EJB با حالت تبدیل می‌کند. یک EJB دارای حالت (stateful EJB) دارای فیلدهای خصوصی است که مقادیرشان باید در طول زمان حفظ شود. یک مثال کلاسیک، کلاسی است که حاوی اطلاعاتی در مورد کاربر یک برنامه وب است. یک نمونه از این کلاس با یک کاربر خاص مرتبط است، و هنگامی که نخ اجرایی یک درخواست از آن کاربر به پایان رسید، آن نمونه باید حفظ شود تا برای درخواست بعدی از همان کلاینت در دسترس باشد. یک EJB @Stateless فاقد حالت است. برای استفاده از همین مثال، در پایان اجرای نخ (thread) درخواست یک کاربر، EJB @Stateless به استخر EJBهای @Stateless بازمی‌گردد و برای اجرای نخ درخواست کاربر دیگری در دسترس قرار می‌گیرد.
  • برای توسعه‌دهنده، مفهوم EJB 3 @Stateless مشابه مفهوم singleton در Spring است. از آن در شرایط مشابه استفاده می‌شود.
  • خط ۷: تگ @PersistenceContext همان تگی است که در نسخه Spring لایه [dao] یافت می‌شود. این فیلد را مشخص می‌کند که EntityManager را در خود نگه می‌دارد و به لایه [dao] امکان می‌دهد تا زمینه پایداری را مدیریت کند.
  • خط ۱۱: آناوتیشن @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] دقیقاً مشابه پیاده‌سازی مورد بحث در بخش ۳.۱.۴ است، به جز سه مورد جزئی:



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

  • خط ۲: کلاس [Service] یک EJB بدون حالت است
  • خط ۳: تمام متدهای کلاس [Service] باید در داخل یک تراکنش اجرا شوند
  • خطوط ۷–۸: مرجعی به EJB در لایه [dao] توسط کانتینر EJB به فیلد [IDao dao] در خط ۸ تزریق خواهد شد. این تگ @EJB در خط ۷ است که این تزریق را درخواست می‌کند. شیء تزریقی باید یک EJB باشد. این تفاوت قابل توجهی با Spring است، جایی که هر نوع شیء می‌تواند در شیء دیگری تزریق شود.

3.2.5. پیکربندی لایه‌ها

پیکربندی لایه‌های [service]، [dao] و [JPA] توسط فایل‌های زیر انجام می‌شود:

  • [META-INF/persistence.xml] لایه JPA را پیکربندی می‌کند
  • [jboss-config.xml] کانtejینر 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>

        <!-- DataSource JTA که توسط محیط Java EE5 مدیریت می‌شود -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!--جستجو برای اشیاء در لایه JBA -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- لاگ‌های Hibernate SQL
                <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" />

            <!--بازسازی تمام جداول (حذف+ایجاد) هنگام استقرار واحد پایداری -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

این فایل مشابه فایل‌هایی است که هنگام مطالعهٔ اشیاء JPA قبلاً با آن‌ها مواجه شده‌ایم. این فایل یک لایهٔ Hibernate JPA را پیکربندی می‌کند. ویژگی‌های جدید به شرح زیر هستند:

  • خط ۵: واحد پایداری jpa فاقد ویژگی transaction-type است، ویژگی‌ای که تا به حال همیشه می‌دیدیم:

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

در صورت عدم وجود مقدار، ویژگی transaction-type دارای مقدار پیش‌فرض "JTA" (برای API تراکنش جاوا) است که نشان می‌دهد مدیر تراکنش توسط یک کانtejner EJB 3 فراهم شده است. یک مدیر «JTA» می‌تواند کارهای بیشتری نسبت به یک مدیر «RESOURCE_LOCAL» انجام دهد: این مدیر می‌تواند تراکنش‌هایی را که چندین اتصال را در بر می‌گیرند، مدیریت کند. با JTA، می‌توانید یک تراکنش t1 را روی یک اتصال c1 روی یک SGBD 1 باز کنید، یک تراکنش t2 را روی یک اتصال c2 با یک SGBD 2 باز کنید، و (t1, t2) را به‌عنوان یک تراکنش واحد در نظر بگیرید که در آن یا همه عملیات‌ها با موفقیت به پایان می‌رسند (commit) یا هیچ‌کدام (rollback).

در اینجا، ما با مدیر JTA از کانتینر JBoss EJB3 کار می‌کنیم.

  • خط ۱۱: منبع داده‌ای را که توسط مدیر 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>
  • خط ۳: تگ ریشه فایل <deployment> است. این فایل استقرار عمدتاً برای پیکربندی منبع داده java:/datasource که در persistence.xml اعلام شده بود، در نظر گرفته شده است.
  • منبع داده توسط بین «datasource» در خط ۳۸ تعریف شده است. می‌بینیم که منبع داده (خط ۴۰) از یک «کارخانه» که توسط بین «datasourceFactory» در خط ۷ تعریف شده است، به دست می‌آید. برای به‌دست‌آوردن منبع دادهٔ برنامه، کلاینت باید متد [getDatasource] از کلاس factory را فراخوانی کند (خط ۳۹).
  • خط ۷: factory که منبع داده را فراهم می‌کند، یک کلاس JBoss است.
  • خط ۹: نام منبع داده JNDI است. این نام باید با نام اعلام‌شده در تگ <jta-data-source> در فایل persistence.xml یکسان باشد. در واقع، لایه JPA از این نام، JNDI، برای درخواست منبع داده استفاده خواهد کرد.
  • خطوط ۱۲–۱۵: چیزی استانداردتر: ویژگی‌های JDBC برای اتصال به SGBD
  • خطوط ۱۸–۲۱: پیکربندی استخر اتصال داخلی برای کانتینر JBoss EJB3.
  • خطوط ۲۴–۲۶: مدیر JTA. کلاس [TransactionManager] که در خط ۲۵ تزریق شده، در فایل [embedded-jboss-beans.xml] تعریف شده است.
  • خطوط ۲۸–۳۰: کش Hibernate، مفهومی که هنوز پوشش نداده‌ایم. کلاس [CachedConnectionManager] که در خط 29 تزریق شده است، در فایل [embedded-jboss-beans.xml] تعریف شده است. توجه داشته باشید که پیکربندی اکنون به Hibernate وابسته است، که هنگام مهاجرت به Toplink برای ما مشکل‌ساز خواهد شد.
  • خطوط ۳۲–۳۴: پیکربندی سرویس JNDI.

ما اکنون کار با فایل پیکربندی JBoss EJB3 را به پایان رسانده‌ایم. این فایل پیچیده است و بسیاری از جنبه‌های آن نامشخص باقی مانده‌اند. این فایل از [ref1] گرفته شده است. با این حال، ما قادر خواهیم بود آن را برای SGBD دیگری تطبیق دهیم (خطوط ۱۲–۱۵ از jboss-config.xml، خط ۲۴ از 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);

        // ایجاد Beans مخصوص برنامه
        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());
        }
    }
}
  • روش راه‌اندازی کانtejner JBoss EJB3 در [ref1] یافت شد.
  • خط ۱۳: کانتینر راه‌اندازی می‌شود. [EJB3StandaloneBootstrap] یک کلاس درون کانتینر است.
  • خط 16: واحد استقرار پیکربندی‌شده توسط [jboss-config.xml] در کانتینر مستقر می‌شود: مدیر JTA، منبع داده، استخر اتصال، کش Hibernate و سرویس JNDI راه‌اندازی می‌شوند.
  • خط ۲۲: به کانtejner دستور داده می‌شود تا پوشه «bin» پروژه Eclipse را برای یافتن EJBها اسکن کند. EJBهای لایه‌های [service] و [dao] توسط کانtejner یافت و مدیریت خواهند شد.
  • خط ۲۵: یک زمینه JNDI اولیه می‌شود. این زمینه برای یافتن EJBها استفاده خواهد شد.
  • خط ۲۸: EJB متناظر با کلاس [Service] در لایه [service] از سرویس JNDI درخواست می‌شود. یک EJB می‌تواند به‌صورت محلی یا از طریق شبکه دسترسی پیدا کند. در اینجا، نام «Service/local» EJB مورد نظر به کلاس [Service] در لایه [service] برای دسترسی محلی اشاره دارد.
  • اکنون، برنامه مستقر شده و ما یک مرجع به لایه [service] داریم. ما در همان وضعیتی هستیم که پس از خط ۱۱ در کد [InitDB] نسخه اسپرینگ وجود دارد. بنابراین، ما همان کد را در هر دو نسخه پیدا می‌کنیم.

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();
    }
...
  • خط ۳۶ (JBoss EJB3): کانtejner 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]

از خوانندگان دعوت می‌شود این لاگ‌ها را بررسی کنند. این لاگ‌ها حاوی اطلاعات جالبی در مورد عملکرد کانtejner 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 (خطوط ۱۰–۳۷)، که برای راه‌اندازی محیط مورد نیاز برای تست‌ها استفاده می‌شود، از کدی که قبلاً در [InitDB] توضیح داده شده است، بهره می‌برد.
  • متد `terminate` (خطوط ۴۰–۴۵)، که در پایان تست‌ها (وقتی انوتیشن `@AfterClass` موجود باشد) اجرا می‌شود، کانtejner EJB3 را خاموش می‌کند (خط ۴۴).
  • بقیه موارد دقیقاً مشابه نسخه 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>

        <!-- DataSource JTA که توسط محیط Java مدیریت می‌شود EE5 -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!--جستجو برای اشیاء در لایه JBA -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- لاگ‌های Hibernate SQL
                <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" />

            <!--بازسازی تمام جداول (حذف+ایجاد) هنگام استقرار واحد پایداری -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

فقط یک خط تغییر کرده است:

  • خط ۲۴: گویش 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>

فقط خطوط ۱۲ تا ۱۵ تغییر کرده‌اند: آنها جزئیات اتصال جدید JDBC را مشخص می‌کنند.

از خوانندگان دعوت می‌شود آزمایش‌های توصیف‌شده برای MySQL5 را با سایر فایل‌های SGBD تکرار کنند.

3.2.9. تغییر پیاده‌سازی JPA

همان‌طور که در بالا ذکر شد، ما هیچ مثالی از استفاده از کانtejner JBoss EJB3 با TopLink پیدا نکرده‌ایم. تا ژوئن ۲۰۰۷، من هنوز نمی‌دانم آیا این پیکربندی امکان‌پذیر است یا خیر.

3.3. مثال‌های دیگر

بیایید خلاصه‌ای از آنچه با انتیت Personne انجام شده است ارائه دهیم. ما سه معماری برای اجرای همان تست‌ها ساخته‌ایم:

۱ – پیاده‌سازی Spring/Hibernate

۲ – پیاده‌سازی Spring/Toplink

۳ – پیاده‌سازی JBoss EJB3/Hibernate

مثال‌های این آموزش این سه معماری را به همراه سایر اجزای مطرح‌شده در بخش اول آموزش پوشش می‌دهند:

دسته‌بندی - مقاله

  • در [1]: نسخه اسپرینگ / هایبرنت
  • در [2]: نسخه Spring / TopLink
  • در [3]: نسخه JBoss EJB3 / Hibernate

شخص - آدرس - فعالیت

  • در [1]: نسخه Spring/Hibernate
  • در [2]: نسخه Spring / Toplink
  • [3]: نسخه JBoss EJB3 / Hibernate

این مثال‌ها از نظر معماری هیچ ویژگی جدیدی را معرفی نمی‌کنند. آنها صرفاً در زمینه‌ای کاربرد دارند که چندین موجودیت برای مدیریت وجود دارد و بین آنها روابط یک‌به‌چند یا چند‌به‌چند برقرار است – چیزی که در مثال‌های مربوط به موجودیت Personne وجود ندارد.

3.4. مثال ۳: Spring / JPA در یک برنامه وب

3.4.1. نمای کلی

در اینجا به سراغ برنامه‌ای می‌رویم که در سند زیر ارائه شده است:

[ref4]: مبانی توسعه وب MVC در جاوا [http://tahe.developpez.com/java/baseswebmvc/].

این سند مبانی توسعه وب MVC در جاوا را پوشش می‌دهد. برای درک مثال پیش رو، خواننده باید با این مبانی آشنا باشد. برنامه وب از سرور Tomcat استفاده خواهد کرد. نصب و استفاده از آن در محیط Eclipse در بخش 5.3 توضیح داده شده است.

این برنامه با یک لایه [dao] مبتنی بر ابزار iBatis / SqlMap [http://ibatis.apache.org/] توسعه داده شده بود که پل رابطه‌ای به شیءگرا را فراهم می‌کرد. ما به سادگی Ibatis را با JPA جایگزین می‌کنیم. معماری برنامه به شرح زیر خواهد بود:

اپلیکیشن وبی که قصد داریم بنویسیم، امکان مدیریت گروهی از افراد را با استفاده از چهار عملیات فراهم می‌کند:

  • فهرست کردن افراد گروه
  • افزودن یک شخص به گروه
  • ویرایش یک شخص در گروه
  • حذف یک شخص از گروه

Image

این چهار عملیات پایه‌ای از یک جدول پایگاه داده آشنا هستند. اسکرین‌شات‌های زیر از نشان می‌دهند که برنامه چه صفحاتی را به کاربر نمایش می‌دهد.

 
 

3.4.2. پروژه اکلیپس

پروژه 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] را از این پروژه حفظ می‌کنیم، با این تفاوت که از نمونه Postgres SGBD استفاده خواهیم کرد که منجر به تغییرات زیر در [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>

خطوط ۸ و ۱۶–۱۹ برای 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. پیکربندی برنامه وب

بیایید بار دیگر به معماری پروژه اکلیپس نگاهی بیندازیم:

 
  • در بسته [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>
  • خطوط ۲۳–۲۶: URLهای [/do/*] توسط سرولِت [personnes] پردازش خواهند شد
  • خطوط ۷–۸: سرولت [personnes] نمونه‌ای از کلاس [Application] است، کلاسی که ما قصد داریم ایجاد کنیم.
  • خطوط ۹–۲۰: سه پارامتر [urlList, urlEdit, urlErreurs] را تعریف می‌کنند که آدرس صفحات JSP را برای نماهای [list, edit, erreurs] مشخص می‌نمایند.
  • خطوط ۲۸–۳۰: برنامه دارای یک صفحهٔ اصلی پیش‌فرض به آدرس [index.jsp] است که در ریشهٔ پوشهٔ برنامهٔ وب قرار دارد.
  • خطوط ۳۲–۳۵: برنامه دارای یک صفحهٔ خطای پیش‌فرض است که زمانی نمایش داده می‌شود که وب‌سرور با استثنایی مواجه شود که توسط برنامه مدیریت نشده باشد.
    • خط ۳۷: تگ <exception-type> نوع استثنایی را که توسط دستور <error-page> مدیریت می‌شود مشخص می‌کند؛ در اینجا نوع [java.lang.Exception] و زیرنوع‌های آن، یعنی همه استثناها، است.
    • خط ۳۸: تگ <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] (خط ۴) کلاینت را به 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] است: یک لیست از پیام‌های خطا.
  • خطوط ۳۱–۴۳: لیست ${people} به صورت حلقه‌ای پردازش می‌شود تا آرایه‌ای از نوع HTML که شامل افراد گروه است، نمایش داده شود.
  • خط ۴۰: URL مورد اشاره توسط لینک [Modifier] توسط فیلد [id] از شخص فعلی تنظیم می‌شود تا کنترلری که با URL [/do/edit] مرتبط است بداند کدام شخص قرار است ویرایش شود.
  • خط ۴۱: همین امر در مورد لینک [Supprimer] نیز صدق می‌کند.
  • خط ۳۷: برای نمایش تاریخ تولد شخص در قالب JJ/MM/AAAA، ما از تگ در کتابخانه تگ [DateTime] در پروژه Apache [Jakarta Taglibs] استفاده می‌کنیم:

Image

فایل توضیحات این کتابخانه تگ در خط ۳ تعریف شده است.

  • خط ۴۶: لینک [Ajout] برای افزودن شخص جدید به URL [/do/edit] اشاره می‌کند، درست مانند لینک [Modifier] در خط ۴۰. مقدار -1 پارامتر [id] نشان می‌دهد که این یک افزودن است نه یک ویرایش.
  • خطوط ۱۰–۱۸: اگر عنصر ${errors} در قالب وجود داشته باشد، پیام‌های خطایی که در آن قرار دارند نمایش داده می‌شوند.

نما [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] (خط ۷۳) اقدام POST را برای فرم در URL [/do/validate] (خط ۱۶) فراخوانی می‌کند. اگر 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
خالی
نشان‌دهنده تعداد نادرست فرزندان است – در غیر این صورت خالی می‌ماند
  • رده‌های ۱۱–۱۵: اگر POST در فرم ناموفق باشد، [erreurEdit!=''] بازگردانده شده و یک پیام خطا نمایش داده می‌شود.
  • خط ۱۶: فرم به URL [/do/validate] ارسال خواهد شد
  • خط ۲۰: عنصر [id] قالب نمایش داده می‌شود
  • خط ۲۴: عنصر [version] از قالب نمایش داده می‌شود
  • خطوط ۲۶–۳۲: وارد کردن نام کوچک شخص:
    • هنگامی که فرم برای اولین بار نمایش داده می‌شود (GET)، ${first_name} مقدار فعلی فیلد [prenom] از شیء به‌روز‌شده [Personne] را نمایش می‌دهد، و ${erreurPrenom} خالی است.
    • اگر پس از POST خطایی رخ دهد، مقدار وارد شده ${first_name} همراه با هر پیام خطا ${erreurPrenom} دوباره نمایش داده می‌شود.
  • خطوط ۳۳–۳۹: وارد کردن نام خانوادگی شخص
  • خطوط ۴۰–۴۶: وارد کردن تاریخ تولد شخص
  • خطوط ۴۷–۶۱: وارد کردن وضعیت تأهل شخص با استفاده از دکمه رادیویی. مقدار فیلد [marie] در شیء [Personne] برای تعیین اینکه کدام یک از دو دکمه رادیویی باید انتخاب شود، استفاده می‌شود.
  • خطوط ۶۲–۶۸: وارد کردن تعداد فرزندان شخص
  • خط ۷۱: یک فیلد مخفی HTML با نام [id] که مقدار آن فیلد [id] برای فردی است که در حال به‌روزرسانی است؛ مقدار -۱ برای افزودن، و هر مقدار دیگری برای اصلاح.
  • خط ۷۲: یک فیلد مخفی HTML با نام [version]، با مقداری برابر با فیلد [id] شخص در حال به‌روزرسانی.
  • خط ۷۳: دکمه [Valider] از نوع [Submit] در فرم
  • خط ۷۴: یک پیوند برای بازگشت به فهرست افراد. این پیوند با برچسب [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 گنجانده شود، صفحه باید تگ را در خط ۳ تعریف کرده باشد.
  • خط ۶: کد وضعیت HTTP در پاسخ روی ۲۰۰ تنظیم شده است. این اولین هدر، HTTP، در پاسخ است. کد 200 به کلاینت نشان می‌دهد که درخواست او برآورده شده است. معمولاً یک سند HTML در پاسخ سرور گنجانده می‌شود. در اینجا نیز همین‌طور است. اگر کد وضعیت پاسخ HTTP روی 200 تنظیم نشده باشد، در اینجا مقدار 500 را خواهد داشت که نشان‌دهنده وقوع یک خطا است. این به این دلیل است که سرور وب، پس از دریافت یک استثنای مدیریت‌نشده، این را به عنوان یک وضعیت غیرعادی تشخیص داده و آن را با کد 500 گزارش می‌کند. پاسخ مربوط به کد وضعیت 500 بسته به مرورگر متفاوت است: فایرفاکس سندی را که ممکن است همراه این پاسخ باشد نمایش می‌دهد، در حالی که مرورگرهای دیگر این سند را نادیده گرفته و صفحهٔ خود را نمایش می‌دهند. به همین دلیل ما کد وضعیت 500 را با کد وضعیت 200 جایگزین کرده‌ایم.
  • خط ۱۶: متن استثنا نمایش داده می‌شود
  • خط ۱۸: به کاربر لینکی برای بازگشت به فهرست افراد ارائه می‌شود

نما [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] می‌باشد؛ این اشیاء پیام‌های خطا هستند. این پیام‌ها توسط حلقه در خطوط ۱۳–۱۵ نمایش داده می‌شوند.

3.4.3.3. کنترل‌کنندهٔ برنامه

کنترل‌کننده [Application] در بسته [web] تعریف شده است:

Image


ture و инициалиزاسيون کنترلر


اسکلت کنترل‌کننده [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);
    }

}
  • خطوط ۲۱–۳۴: پارامترهای مورد انتظار از فایل [web.xml] بازیابی می‌شوند.
  • خطوط ۳۷–۳۹: پارامتر [urlErreurs] باید موجود باشد، زیرا URL نمای [erreurs] را مشخص می‌کند که قادر به نمایش هرگونه خطای инициализация است. اگر این پارامتر وجود نداشته باشد، برنامه با اجرای [ServletException] (خط ۳۹) خاتمه می‌یابد. این استثنا به سرور وب منتقل شده و توسط تگ <error-page> در فایل [web.xml] مدیریت می‌شود. بنابراین نما [exception.jsp] نمایش داده می‌شود:

Image

لینک [Retour à la liste] بالا غیرفعال است. استفاده از آن تا زمانی که برنامه اصلاح و دوباره بارگذاری نشود، همان پاسخ را بازخواهد گرداند. همانطور که قبلاً دیدیم، این برای انواع دیگر خطاها مفید است.

  • خطوط ۴۰–۴۳: از فایل پیکربندی 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);
    }
  • خطوط ۷–۱۳: بررسی می‌کنیم که لیست خطاهای инициализация خالی باشد. اگر اینطور نباشد، نمای [erreurs(erreurs)] را نمایش می‌دهیم که خطا(ها) را گزارش خواهد کرد.
  • خط ۱۵: متدی که توسط کلاینت برای ارسال درخواستش استفاده می‌شود، یعنی [get] یا [post]، بازیابی می‌شود.
  • خط ۱۷: مقدار پارامتر [action] را از درخواست بازیابی می‌کنیم.
  • خطوط ۲۳–۲۷: پردازش درخواست [GET /do/list] که درخواست فهرستی از افراد را دارد.
  • خطوط ۲۸–۳۲: پردازش درخواست [GET /do/delete] که درخواست حذف یک شخص را دارد.
  • خطوط ۳۳–۳۷: پردازش پرس‌وجوی [GET /do/edit] که فرم به‌روزرسانی یک شخص را درخواست می‌کند.
  • خطوط ۳۸–۴۲: پردازش درخواست [POST /do/validate] که درخواست اعتبارسنجی شخص به‌روزرسانی‌شده را دارد.
  • خط ۴۴: اگر عمل درخواستی یکی از پنج مورد قبلی نباشد، سیستم آن را به‌عنوان [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);
}
  • خط ۴: لایه [service] برای دریافت فهرست افراد گروه پرس‌وجو می‌شود و این فهرست در مدل تحت کلید «people» قرار می‌گیرد.
  • خط ۶: نمای [list.jsp]، که در بند 3.4.3.2 توصیف شده است، نمایش داده می‌شود.

روش [doDeletePersonne]


این متد پرس‌وجوی [GET /do/delete?id=XX] را پردازش می‌کند که درخواست حذف شخص با شناسه 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>

در خط ۱۲، ما 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");
    }
  • خط ۴: URL در حال پردازش به شکل [/do/delete?id=XX] است. مقدار [XX] از پارامتر [id] بازیابی می‌شود.
  • خط ۶: لایه [service] دستور داده می‌شود تا شخص با شناسهٔ به‌دست‌آمده را حذف کند. ما هیچ‌گونه بررسی‌ای انجام نمی‌دهیم. اگر شخصی که قصد حذف آن را داریم وجود نداشته باشد، لایه [dao] یک استثنا (exception) ایجاد می‌کند که توسط لایه [service] به سمت بالا منتقل می‌شود. ما اینجا در کنترلر نیز این مورد را مدیریت نمی‌کنیم. بنابراین این خطا به سمت سرور وب ارسال می‌شود که با پیکربندی، صفحه [exception.jsp] را که در بند 3.4.3.2 توصیف شده است، نمایش خواهد داد:

Image

  • خط ۹: اگر حذف انجام شده باشد (بدون استثنا)، به کلاینت دستور داده می‌شود که به URL نسبی [list] هدایت (redirect) کند. از آنجایی که صفحهٔ به‌تازگی پردازش‌شده [/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>

در خط ۱۱، URL [/do/edit?id=XX] برای لینک [Modifier]، و در خط ۱۷، 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] را هدف قرار می‌دهد. در خط ۴، مقدار [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]. این عناصر در خطوط ۱۹ تا ۳۱ مقداردهی اولیه می‌شوند، به استثنای آن‌هایی که مقدارشان رشتهٔ خالی [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants] است. مشخص است که اگر آن‌ها در قالب وجود نداشته باشند، کتابخانه JSTL برای مقدارشان یک رشتهٔ خالی نمایش خواهد داد. اگرچه عنصر [erreurEdit] نیز مقدار رشته‌ی خالی دارد، با این حال مقداردهی اولیه می‌شود زیرا در صفحه‌ی [edit.jsp]، مقدار آن بررسی می‌شود.
  • هنگامی که مدل آماده شد، کنترل به خط ۳۳ صفحه [edit.jsp] واگذار می‌شود که نمای [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);
    }
  • خطوط ۷–۱۳: پارامتر [prenom] از درخواست POST بازیابی شده و اعتبار آن بررسی می‌شود. اگر نادرست تشخیص داده شود، عنصر [erreurPrenom] با یک پیام خطا مقداردهی اولیه شده و در ویژگی‌های پرس‌وجو قرار می‌گیرد.
  • خطوط ۱۵–۲۱: همین رویه برای پارامتر [nom] دنبال می‌شود
  • خطوط ۲۳–۳۰: همین رویه برای پارامتر [datenaissance] دنبال می‌شود
  • خط ۳۲: پارامتر [marie] بازیابی می‌شود. ما اعتبار آن را بررسی نمی‌کنیم زیرا اصولاً از مقدار یک دکمه رادیویی می‌آید. با این حال، هیچ چیز مانع از آن نمی‌شود که یک برنامه یک [POST /.../do/validate] را به همراه یک پارامتر [marie] ساختگی تولید کند. بنابراین باید اعتبار این پارامتر را بررسی کنیم. در اینجا، ما به مدیریت خطاهای خود تکیه می‌کنیم که در صورتی که کنترل‌کننده خود خطاها را مدیریت نکند، نمایش صفحه [exception.jsp] را فعال می‌کند. بنابراین، اگر تبدیل پارامتر [marie] به یک مقدار بولی در خط ۳۲ با شکست مواجه شود، یک استثنا پرتاب خواهد شد و در نتیجه صفحه [exception.jsp] برای کلاینت ارسال می‌شود. این رفتار برای ما قابل قبول است.
  • خطوط ۳۴–۵۰: ما پارامتر [nbenfants] را بازیابی کرده و مقدار آن را بررسی می‌کنیم.
  • خط ۵۲: ما پارامتر [id] را بدون بررسی مقدار آن بازیابی می‌کنیم
  • خطوط ۵۴–۵۹: اگر فرم حاوی خطا باشد، با پیام‌های خطایی که قبلاً تولید شده‌اند مجدداً نمایش داده می‌شود
  • خطوط ۶۲–۶۷: اگر معتبر باشد، یک شیء جدید [Personne] با استفاده از عناصر فرم ایجاد می‌شود
  • خطوط ۶۹–۸۲: شخص ذخیره می‌شود. عملیات ذخیره ممکن است با شکست مواجه شود. در یک محیط چندکاربره، شخص مورد ویرایش ممکن است حذف شده باشد یا قبلاً توسط شخص دیگری ویرایش شده باشد. در این صورت، لایه [dao] یک استثنا (exception) پرتاب می‌کند که در اینجا مدیریت می‌شود.
  • خط ۸۴: اگر هیچ استثنایی رخ نداده باشد، کلاینت به URL [/do/list] برای نمایش وضعیت جدید گروه هدایت می‌شود.
  • خط ۷۹: اگر در حین ذخیره کردن خطایی رخ داده باشد، درخواست می‌کنیم فرم اولیه مجدداً نمایش داده شود و پیام خطای استثناء (پارامتر سوم) به آن ارسال گردد.

متد [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. نسخهٔ ۲

ما یک تغییر جزئی در نسخه قبلی ایجاد می‌کنیم تا به جای کدهای منبع، از بایگانی لایه‌های [service, dao, jpa] استفاده کنیم:

  • در [1]: پروژه جدید اکلیپس. توجه داشته باشید که بسته‌های [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>
  • خط ۷: انتیتی 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 تنها چند خط نیاز به تغییر دارند:

  • خط ۱۱: پیاده‌سازی JPA اکنون توسط Toplink انجام می‌شود
  • خط ۱۳: ویژگی [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. مثال‌های بیشتر

ما دوست داشتیم یک مثال وب را نشان دهیم که در آن کانtejner Spring با کانtejner JBoss EJB3 مورد بحث در بخش 3.2 جایگزین شده است:

  • در [1]: پروژهٔ Eclipse
  • در [3]: مکان آن در پوشهٔ examples. آن را وارد خواهیم کرد.

ما از پیکربندی [jboss-config.xml, persistence.xml] که در بخش ۳.۲ توضیح داده شده است، مجدداً استفاده کرده و سپس متد [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);

            // ایجاد Beans مخصوص برنامه
            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);
        }
    }
  • خطوط ۲۸–۳۸: کانtejner Ejb3 راه‌اندازی می‌شود. این جایگزین کانtejner Spring می‌شود.
  • خط ۴۱: ما یک مرجع به لایه [service] برنامه درخواست می‌کنیم.

اصولاً این تنها تغییراتی هستند که لازم است. پس از اجرا، خطای زیر رخ می‌دهد:

 

من نتوانسته‌ام دقیقاً بفهمم مشکل از کجاست. استثنایی که توسط Tomcat گزارش شده، به نظر می‌رسد نشان می‌دهد که شیئی به نام «TransactionManager» از سرویس JNDI درخواست شده و این سرویس آن را شناسایی نکرده است. حل این مشکل را به خوانندگان واگذار می‌کنم. اگر راه‌حلی پیدا شود، در این سند گنجانده خواهد شد.