Skip to content

17. برنامه وب MVC در معماری سه‌لایه – مثال ۳ – سامانه مدیریت پایگاه داده فایربرد

17.1. پایگاه داده Firebird

در این نسخه جدید، ما فهرست افراد را در یک جدول پایگاه داده Firebird ذخیره خواهیم کرد. سند [http://tahe.developpez.com/divers/sql-firebird/] حاوی اطلاعاتی در مورد نحوه نصب و مدیریت این SGBD است. اسکرین‌شات‌های زیر از IBExpert، یک کلاینت مدیریتی برای Interbase و Firebird SGBD، گرفته شده‌اند.

این پایگاه داده [dbpersonnes.gdb] نام دارد. این پایگاه داده شامل جدولی به نام [PERSONNES] است:

Image

جدول [PERSONNES] شامل فهرست افرادی است که توسط وب‌اپلیکیشن مدیریت می‌شوند. این جدول با استفاده از دستورات زیر SQL ایجاد شده است:

CREATE TABLE PERSONNES (
    ID             INTEGER NOT NULL,
    "VERSION"      INTEGER NOT NULL,
    NOM            VARCHAR(30) NOT NULL,
    PRENOM         VARCHAR(30) NOT NULL,
    DATENAISSANCE  DATE NOT NULL,
    MARIE          SMALLINT NOT NULL,
    NBENFANTS      SMALLINT NOT NULL
);


ALTER TABLE PERSONNES ADD CONSTRAINT CHK_PRENOM_PERSONNES check (PRENOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_MARIE_PERSONNES check (MARIE=0 OR MARIE=1);
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_NOM_PERSONNES check (NOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_ENFANTS_PERSONNES check (NBENFANTS>=0);


ALTER TABLE PERSONNES ADD CONSTRAINT PK_PERSONNES PRIMARY KEY (ID);
  • رده‌های ۲–۱۰: ساختار جدول [PERSONNES]، که برای ذخیرهٔ اشیاء از نوع [Personne] در نظر گرفته شده است، ساختار آن شیء را منعکس می‌کند. از آنجایی که نوع داده بولی در فایربرد وجود ندارد، فیلد [MARIE] (خط ۸) به عنوان نوع [SMALLINT]، یک عدد صحیح، اعلام شده است. مقدار آن ۰ (مجرد) یا ۱ (متأهل) خواهد بود.
  • خطوط ۱۳–۱۶: محدودیت‌های یکپارچگی که بازتاب‌دهنده محدودیت‌های اعتبارسنج داده‌های [ValidatePersonne] هستند.
  • خط ۱۹: فیلد ID کلید اصلی جدول [PERSONNES] است.

جدول [PERSONNES] ممکن است شامل موارد زیر باشد:

Image

علاوه بر جدول [PERSONNES]، پایگاه داده [dbpersonnes.gdb] شامل ابجکتی به نام ژنراتور به نام [GEN_PERSONNES_ID] است. این ژنراتور اعداد صحیح متوالی تولید می‌کند که از آن‌ها برای تخصیص مقدار به کلید اصلی [ID] از کلاس [PERSONNES] استفاده خواهیم کرد. بیایید برای روشن شدن نحوه کار یک مثال بزنیم:

می‌توانیم ببینیم که مقدار ژنراتور [GEN_PERSONNES_ID] تغییر کرده است (برای تازه‌سازی روی آن دوبار کلیک کنید + F5):

 

sequence SQL

SELECT GEN_ID ( GEN_PERSONNES_ID,1 ) FROM RDB$DATABASE

بنابراین این مقدار زیر برای ژنراتور حاصل می‌شود: [GEN_PERSONNES_ID]. GEN_ID یک تابع داخلی Firebird است و [RDB$DATABASE] یک جدول سیستمی برای SGBD است.

17.2. پروژه اکلیپس برای لایه‌های [dao] و [service]

برای توسعه لایه‌های [dao] و [service] از برنامه کاربردی پایگاه داده خود، از پروژه اکلیپس زیر، [mvc-personnes-03]، استفاده خواهیم کرد:

Image

این پروژه یک پروژه ساده جاوا است، نه یک پروژه وب تامکت. به یاد داشته باشید که نسخه ۲ برنامه ما از لایه [web] نسخه ۱ استفاده خواهد کرد. بنابراین نیازی به نوشتن این لایه نیست.


پوشه [src]


این پوشه حاوی کد منبع لایه‌های [dao] و [service] است:

Image

این پوشه شامل بسته‌های مختلفی است:

  • [istia.st.mvc.personnes.dao]: شامل لایه [dao] است
  • [istia.st.mvc.personnes.entites]: شامل کلاس [Personne] است
  • [istia.st.mvc.personnes.service]: حاوی کلاس [service] است
  • [istia.st.mvc.personnes.tests]: شامل تست‌های JUnit برای لایه‌های [dao] و [service]

و همچنین فایل‌های پیکربندی که باید در دایرکتوری ClassPath برنامه قرار گیرند.


پوشه [database]


این پوشه شامل پایگاه داده افراد Firebird است:

Image

  • [dbpersonnes.gdb] پایگاه داده است.
  • [dbpersonnes.sql] اسکریپت SQL برای تولید پایگاه داده است:
/******************************************************************************/
/***           تولید شده توسط IBExpert ۷ مارس ۲۰۰۶ ۲۷ آوریل          ۲۰۰۶ ۱۰:۲۷:۱۱ ***/
/******************************************************************************/

SET SQL DIALECT 3;

SET NAMES NONE;

CREATE DATABASE 'C:\data\2005-2006\webjava\dvp-spring-mvc\mvc-38\database\DBPERSONNES.GDB'
USER 'SYSDBA' PASSWORD 'masterkey'
PAGE_SIZE 16384
DEFAULT CHARACTER SET NONE;



/******************************************************************************/
/***                                                               تولیدکننده‌ها ***/
/******************************************************************************/

CREATE GENERATOR GEN_PERSONNES_ID;
SET GENERATOR GEN_PERSONNES_ID TO 787;



/******************************************************************************/
/***                                                                   جداول ***/
/******************************************************************************/



CREATE TABLE PERSONNES (
    ID             INTEGER NOT NULL,
    "VERSION"      INTEGER NOT NULL,
    NOM            VARCHAR(30) NOT NULL,
    PRENOM         VARCHAR(30) NOT NULL,
    DATENAISSANCE  DATE NOT NULL,
    MARIE          SMALLINT NOT NULL,
    NBENFANTS      SMALLINT NOT NULL
);

INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (1, 1, 'Major', 'Joachim', '1984-11-13', 1, 2);
INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (2, 1, 'Humbort', 'Mélanie', '1985-02-12', 0, 1);
INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (3, 1, 'Lemarchand', 'Charles', '1986-03-01', 0, 0);

COMMIT WORK;



/*بررسی تعاریف محدودیت‌ها */

ALTER TABLE PERSONNES ADD CONSTRAINT CHK_PRENOM_PERSONNES check (PRENOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_NOM_PERSONNES check (NOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_MARIE_PERSONNES check (MARIE=0 OR MARIE=1);
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_ENFANTS_PERSONNES check (NBENFANTS>=0);


/******************************************************************************/
/***                                                             کلیدهای اصلی ***/
/******************************************************************************/

ALTER TABLE PERSONNES ADD CONSTRAINT PK_PERSONNES PRIMARY KEY (ID);

پوشه [lib]


این پوشه شامل آرشیوهای مورد نیاز برنامه است:

شایان ذکر است که درایور JDBC [firebirdsql-full.jar] برای SGBD Firebird، و همچنین تعدادی آرشیو [spring-*.jar] موجود است. می‌توانستیم از آرشیو واحد [spring.jar] موجود در پوشه [dist] توزیع استفاده کنیم که شامل تمام کلاس‌های Spring است. همچنین می‌توان تنها از آرشیوهای لازم برای پروژه استفاده کرد. این کاری است که ما در اینجا انجام داده‌ایم، با راهنمایی خطاهای «کلاس مفقود» گزارش‌شده توسط Eclipse و نام‌های آرشیوهای جزئی Spring. تمام این آرشیوها از پوشه [lib] در پوشه Classpath پروژه قرار داده شده‌اند.


پوشه [dist]


این پوشه حاوی آرشیوهای حاصل از کامپایل شدن کلاس‌های برنامه خواهد بود:

Image

  • [personnes-dao.jar]: آرشیو لایه [dao]
  • [personnes-service.jar]: آرشیو لایه [service]

17.3. لایه [dao]

17.3.1. اجزای لایه [dao]

لایه [dao] شامل کلاس‌ها و رابط‌های زیر است:

Image

  • [IDao] رابطی است که توسط لایه [dao] ارائه می‌شود
  • [DaoImplCommon] پیاده‌سازی این رابط است که در آن گروه افراد در یک جدول پایگاه داده ذخیره می‌شود. [DaoImplCommon] عملکردهایی را که از SGBD مستقل هستند، در کنار هم قرار می‌دهد.
  • [DaoImplFirebird] کلاسی مشتق‌شده از [DaoImplCommon] است که به‌طور خاص برای مدیریت پایگاه داده Firebird طراحی شده است.
  • [DaoException] نوع استثناهای رسیدگی‌نشده‌ای است که توسط لایه [dao] پرتاب می‌شوند. این کلاس با نسخه ۱ مطابقت دارد.

رابط [IDao] به شرح زیر است:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
     // فهرست تمام افراد
    Collection getAll();
     //بازیابی یک شخص خاص
    Personne getOne(int id);
     // افزودن/ویرایش یک شخص
    void saveOne(Personne personne);
     // حذف یک شخص
    void deleteOne(int id);
}
  • این رابط همان چهار متد نسخه قبلی را دارد.

کلاس [DaoImplCommon] که این رابط را پیاده‌سازی می‌کند، به شرح زیر خواهد بود:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

     // فهرست افراد
    public Collection getAll() {
...
    }

     //بازیابی یک شخص خاص
    public Personne getOne(int id) {
...
    }

     // حذف یک شخص
    public void deleteOne(int id) {
...
    }

     // افزودن یا ویرایش یک شخص
    public void saveOne(Personne personne) {
         //آیا پارامتر 'person' معتبر است؟
        check(personne);
         // افزودن یا اصلاح؟
        if (personne.getId() == -1) {
             // افزودن
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

     // افزودن یک شخص
    protected void insertPersonne(Personne personne) {
...
    }

     // ویرایش یک شخص
    protected void updatePersonne(Personne personne) {
...
    }

     //بررسی اعتبار یک شخص
    private void check(Personne p) {
...
    }

...
}
  • خطوط ۸–۹: کلاس [DaoImpl] رابط [IDao] را پیاده‌سازی می‌کند و بنابراین چهار متد [getAll, getOne, saveOne, deleteOne] را پیاده‌سازی می‌کند.
  • خطوط ۲۷–۳۷: متد [saveOne] بسته به اینکه یک شخص در حال اضافه شدن یا ویرایش است، از دو متد داخلی [insertPersonne] و [updatePersonne] استفاده می‌کند.
  • خط ۵۰: متد خصوصی [check] همان متد نسخه قبلی است. در اینجا مجدداً به آن نمی‌پردازیم.
  • خط ۸: برای پیاده‌سازی رابط [IDao]، کلاس [DaoImpl] از کلاس Spring با نام [SqlMapClientDaoSupport] ارث می‌برد.

17.3.2. لایه دسترسی به داده [iBATIS]

کلاس Spring با نام [SqlMapClientDaoSupport] از فریم‌ورک شخص ثالث [Ibatis SqlMap] که در آدرس URL [http://ibatis.apache.org/] در دسترس است، استفاده می‌کند:

Image

[iBATIS] یک پروژه آپاچی است که ساخت لایه‌های [dao] مبتنی بر پایگاه‌داده‌ها را تسهیل می‌کند. با [iBATIS]، معماری لایه دسترسی به داده به شرح زیر است:

[iBATIS] بین لایه [dao] برنامه و درایور JDBC پایگاه داده قرار دارد. برای [iBATIS] جایگزین‌هایی وجود دارد، مانند، برای مثال، جایگزین [Hibernate]:

Image

استفاده از چارچوب [iBATIS] نیازمند دو آرشیو [ibatis-common, ibatis-sqlmap] است که هر دو در پوشه [lib] پروژه قرار داده شده‌اند:

کلاس [SqlMapClientDaoSupport] بخش عمومی فریم‌ورک‌های [iBATIS] و c.a.d را در بر می‌گیرد. بخش‌های کدی که در تمام لایه‌های [dao] با استفاده از ابزار [iBATIS] یافت می‌شوند. برای نوشتن بخش غیرکلی کد – یعنی بخشی که مختص لایه [dao] است – کافی است از کلاس [SqlMapClientDaoSupport] ارث‌بری کنید. این کاری است که ما در اینجا انجام می‌دهیم.

کلاس [SqlMapClientDaoSupport] به صورت زیر تعریف شده است:

Image

در میان متدهای این کلاس، یکی از آن‌ها به شما امکان می‌دهد کلاینت [iBATIS] را که با آن به پایگاه داده دسترسی خواهید داشت، پیکربندی کنید:

Image

شیء [SqlMapClient sqlMapClient] همان شیء [IBATIS] است که برای دسترسی به پایگاه داده استفاده می‌شود. به خودی خود، این شیء لایه [iBATIS] معماری ما را پیاده‌سازی می‌کند:

یک توالی معمول از عملیات شامل این شیء به شرح زیر است:

  1. درخواست اتصال از استخر اتصالات
  2. باز کردن یک تراکنش
  3. اجرای مجموعه‌ای از دستورات SQL که در یک فایل پیکربندی ذخیره شده‌اند
  4. بستن تراکنش
  5. بازگرداندن اتصال به استخر

اگر پیاده‌سازی ما از [DaoImplCommon] قرار بود مستقیماً با [iBATIS] کار کند، باید این توالی را بارها و بارها اجرا می‌کرد. تنها عملیات ۳ به لایه [dao] اختصاص دارد؛ سایر عملیات‌ها عمومی هستند. کلاس Spring [SqlMapClientDaoSupport] عملیات ۱، ۲، ۴ و ۵ را خود مدیریت می‌کند و عملیات ۳ را به کلاس مشتق خود، در این مورد کلاس [DaoImplCommon]، واگذار می‌کند.

برای کارکرد، کلاس [SqlMapClientDaoSupport] به یک مرجع به شی iBATIS [SqlMapClient sqlMapClient] نیاز دارد که ارتباط با پایگاه داده را مدیریت خواهد کرد. این شی برای کارکرد به دو چیز نیاز دارد:

  • یک شیء [DataSource] متصل به پایگاه داده، که از آن درخواست اتصال خواهد کرد
  • یک (یا چند) فایل پیکربندی که در آن‌ها دستورات SQL برای اجرا به صورت خارجی تعریف شده‌اند. این دستورات در واقع در داخل کد جاوا قرار ندارند. آنها توسط کدی در یک فایل پیکربندی شناسایی می‌شوند و شیء [SqlMapClient sqlMapClient] از این کد برای اجرای یک دستور SQL خاص استفاده می‌کند.

یک پیکربندی اولیه برای لایه [dao] ما، که معماری توصیف‌شده در بالا را منعکس می‌کند، به شرح زیر خواهد بود:


    <!-- دسترسی کلاس‌ها برای لایه [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
</bean>

در اینجا، خاصیت [sqlMapClient] (خط ۳) از کلاس [DaoImplCommon] (خط ۲) مقداردهی اولیه می‌شود. این خاصیت توسط متد [setSqlMapClient] از کلاس [DaoImpl] مقداردهی اولیه می‌شود. این کلاس این متد را ندارد. کلاس والد آن، [SqlMapClientDaoSupport]، این متد را دارد. بنابراین، در اینجا در واقع کلاس والد است که مقداردهی اولیه می‌شود.

اکنون، در خط ۴، به شیئی به نام «sqlMapClient» ارجاع داده شده است که هنوز ساخته نشده است. همانطور که ذکر شد، این شیء از نوع [SqlMapClient] است، که نوعی از [iBATIS] است:

Image

[SqlMapClient] یک رابط است. Spring کلاس [SqlMapClientFactoryBean] را برای به‌دست‌آوردن یک شیء که این رابط را پیاده‌سازی می‌کند، فراهم می‌کند:

Image

به یاد داشته باشید که ما در پی ایجاد یک شیء هستیم که رابط [SqlMapClient] را پیاده‌سازی می‌کند. به نظر نمی‌رسد این مورد برای کلاس [SqlMapClientFactoryBean] صدق کند. این کلاس رابط [FactoryBean] را پیاده‌سازی می‌کند (به بالا مراجعه کنید). این کلاس متد زیر را دارد، [getObject()]:

Image

وقتی از Spring خواسته می‌شود نمونه‌ای از یک شیء پیاده‌کننده رابط [FactoryBean] را فراهم کند، آن:

  • یک نمونه از کلاس [I] ایجاد می‌کند – در این مورد، یک نمونه از نوع [SqlMapClientFactoryBean] ایجاد می‌کند.
  • نتیجهٔ متد `[I].getObject()` را به متد فراخوانی‌کننده بازمی‌گرداند – متد `[SqlMapClientFactoryBean].getObject() یک شیء پیاده‌سازی‌کنندهٔ رابط [SqlMapClient] را بازمی‌گرداند.

برای بازگرداندن یک شیء پیاده‌سازی‌کنندهٔ رابط [SqlMapClient]، کلاس [SqlMapClientFactoryBean] به دو مورد اطلاعات لازم برای این شیء نیاز دارد:

  • یک شیء [DataSource] متصل به پایگاه داده، که از آن درخواست اتصال خواهد کرد
  • یک (یا چند) فایل پیکربندی حاوی دستورات SQL برای اجرا

کلاس [SqlMapClientFactoryBean] متدهایی را برای مقداردهی اولیه این دو ویژگی فراهم کرده است:

Image

ما در حال پیشرفت هستیم… فایل پیکربندی ما در حال شکل‌گیری است و اکنون این‌گونه خوانده می‌شود:


<!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- کلاس‌های دسترسی برای لایه [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
  • خطوط ۲–۳: بین «sqlMapClient» از نوع [SqlMapClientFactoryBean] است. از توضیحات فوق، می‌دانیم که وقتی از Spring یک نمونه از این bean را درخواست می‌کنیم، یک شیء پیاده‌سازی‌کنندهٔ رابط iBATIS [SqlMapClient] دریافت می‌کنیم. بنابراین این شیء دوم است که در خط ۱۴ دریافت خواهد شد.
  • خطوط ۷–۹: ما مشخص می‌کنیم که فایل پیکربندی مورد نیاز برای شیء iBATIS [SqlMapClient] با نام «sql-map-config-firebird.xml» است و باید در ClassPath برنامه جستجو شود. متد [SqlMapClientFactoryBean].setConfigLocation در اینجا استفاده می‌شود.
  • خطوط ۴–۶: ما ویژگی [dataSource] از [SqlMapClientFactoryBean] را با استفاده از متد [setDataSource] آن مقداردهی اولیه می‌کنیم.

در خط ۵، به یک بین به نام «dataSource» اشاره می‌کنیم که هنوز ایجاد نشده است. اگر به پارامتری که متد [setDataSource] از [SqlMapClientFactoryBean] انتظار دارد نگاه کنیم، می‌بینیم که از نوع [DataSource] است:

Image

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

  • باز کردن یک اتصال
  • شروع یک تراکنش
  • صدور دستورات SQL
  • تعامل را ببندیم
  • اتصال را بستن

باز و بسته کردن مکرر اتصالات، وقت‌گیر است. برای حل این دو مشکل (هم محدود کردن تعداد اتصالات باز در هر لحظه و هم به حداقل رساندن هزینه باز و بسته کردن آن‌ها)، کلاس‌هایی که رابط [DataSource] را پیاده‌سازی می‌کنند، اغلب به شرح زیر عمل می‌کنند:

  • در هنگام نمونه‌سازی، آن‌ها N اتصال به پایگاه داده مقصد را باز می‌کنند. N معمولاً یک مقدار پیش‌فرض دارد و معمولاً می‌توان آن را در یک فایل پیکربندی تعریف کرد. این N اتصال همیشه باز می‌مانند و یک استخر اتصال را برای نخ‌های برنامه فراهم می‌کنند.
  • هنگامی که یک نخ برنامه (application thread) درخواست اتصال می‌دهد، شیء [DataSource] در صورت باقی ماندن اتصال‌های در دسترس، یکی از N اتصال باز شده در زمان راه‌اندازی را در اختیار آن قرار می‌دهد. هنگامی که برنامه اتصال را می‌بندد، آن اتصال در واقع بسته نمی‌شود، بلکه صرفاً به استخر اتصالات در دسترس بازگردانده می‌شود.

پیاده‌سازی‌های مختلفی از رابط [DataSource] به صورت رایگان در دسترس هستند. در اینجا، ما از پیاده‌سازی [commons DBCP] که در آدرس URL [http://jakarta.apache.org/commons/dbcp/] در دسترس است، استفاده خواهیم کرد:

Image

استفاده از ابزار [commons DBCP] به دو آرشیو [commons-dbcp, commons-pool] نیاز دارد که هر دو در پوشه [lib] پروژه قرار داده شده‌اند:

کلاس [BasicDataSource] از [commons DBCP] پیاده‌سازی [DataSource] مورد نیاز ما را فراهم می‌کند:

Image

این کلاس یک استخر اتصال برای دسترسی به پایگاه داده Firebird برنامه ما [dbpersonnes.gdb] را فراهم می‌کند. برای این کار، باید اطلاعات لازم برای ایجاد اتصالات در استخر را در اختیار آن قرار دهیم:

  1. نام درایور مورد استفاده – مقداردهی اولیه شده با
  2. آدرس URL پایگاه داده مورد استفاده – مقدار اولیه [setUrl]
  3. نام کاربری کاربری که مالک اتصال است – مقدار اولیه [setUsername] (و نه setUserName همانطور که ممکن بود انتظار رود)
  4. رمز عبور آن‌ها – مقدار اولیه [setPassword]

فایل پیکربندی لایه [dao] ما می‌تواند به شکل زیر باشد:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!--منبع داده DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- لطفاً توجه داشته باشید: بین دو تگ <value> در URL هیچ فاصله‌ای قرار ندهید -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!--SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!--کلاس دسترسی برای لایه [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • خطوط ۷–۹: نام درایور JDBC برای پایگاه داده Firebird SGBD
  • خطوط ۱۱–۱۳: URL پایگاه داده Firebird [dbpersonnes.gdb]. هنگام وارد کردن آن دقت کنید. نباید هیچ فاصله‌ای بین تگ‌های <value> و URL وجود داشته باشد.
  • خطوط ۱۴–۱۶: مالک اتصال – در این مورد، [sysdba]، که مدیر پیش‌فرض توزیع‌های Firebird است
  • خطوط 17–19: رمز عبور آن، [masterkey] – که همچنین مقدار پیش‌فرض است

ما پیشرفت خوبی داشته‌ایم، اما هنوز چند مورد پیکربندی وجود دارد که باید روشن شوند: خط ۲۸ به فایل [sql-map-config-firebird.xml] اشاره دارد که برای پیکربندی کلاینت [SqlMapClient] برای iBATIS در نظر گرفته شده است. قبل از بررسی محتویات آن، بیایید ببینیم این فایل‌های پیکربندی در پروژهٔ Eclipse ما کجا قرار دارند:

Image

  • [spring-config-test-dao-firebird.xml] فایل پیکربندی لایه [dao] است که همین‌اکنون بررسی کردیم.
  • [sql-map-config-firebird.xml] توسط [spring-config-test-dao-firebird.xml] ارجاع شده است. آن را بررسی خواهیم کرد.
  • [personnes-firebird.xml] توسط [sql-map-config-firebird.xml] ارجاع شده است. آن را بررسی خواهیم کرد.

سه فایل قبلی در پوشه [src] قرار دارند. در اکلیپس، این بدان معناست که در زمان اجرا، آن‌ها در پوشه [bin] پروژه (که در بالا نشان داده نشده است) موجود خواهند بود. این پوشه بخشی از ClassPath برنامه کاربردی است. در نهایت، سه فایل مذکور در پوشه ClassPath برنامه قرار خواهند داشت. این امر ضروری است.

فایل [sql-map-config-firebird.xml] به شرح زیر است:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
    <sqlMap resource="personnes-firebird.xml"/>
</sqlMapConfig>
  • این فایل باید دارای <sqlMapConfig> به عنوان تگ ریشه باشد (خطوط ۶ و ۸)
  • خط ۷: تگ <sqlMap> برای شناسایی فایل‌هایی که حاوی دستورات SQL هستند، استفاده می‌شود. اغلب، هرچند نه لزوماً، برای هر جدول یک فایل وجود دارد. این امر امکان می‌دهد دستورات SQL مربوط به یک جدول مشخص در یک فایل واحد گروهبندی شوند. با این حال، دستورات SQL که شامل چندین جدول هستند، اغلب مشاهده می‌شوند. در چنین مواردی ساختار فوق اعمال نمی‌شود. کافی است به خاطر داشته باشید که تمام فایل‌هایی که با تگ‌های <sqlMap> مشخص شده‌اند، ادغام خواهند شد. این فایل‌ها از فایل ClassPath برنامه بازیابی می‌شوند.

فایل [personnes-firebird.xml] دستورات SQL را که به جدول [PERSONNES] در پایگاه داده Firebird [dbpersonnes.gdb] صادر خواهند شد، توصیف می‌کند. محتویات آن به شرح زیر است:


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

<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">

<sqlMap>
    <!-- کلاس همنام [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- جدول نگاشت [PERSONNES] - شیء [Personne] -->
    <resultMap id="Personne.map" 
        class="Personne.classe">
        <result property="id" column="ID" />
        <result property="version" column="VERSION" />
        <result property="nom" column="NOM"/>
        <result property="prenom" column="PRENOM"/>
        <result property="dateNaissance" column="DATENAISSANCE"/>
        <result property="marie" column="MARIE"/>
        <result property="nbEnfants" column="NBENFANTS"/>
    </resultMap>
    <!-- فهرست همه افراد -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!--بازیابی یک شخص خاص -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#مقدار#</select>
    <!-- افزودن یک شخص -->
    <insert id="Personne.insertOne" parameterClass="Personne.classe">
        <selectKey keyProperty="id">
            SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
        </selectKey>         
        insert into 
        PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#id#, #نسخه#, #نام_خانوادگی#, #نام#, #dateNaissance#, #نام_خانوادگی_ازدواجی#, 
        #nbEnfants#) </insert>
    <!-- به‌روزرسانی یک شخص -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#نسخه#+1, NOM=#نام خانوادگی#, PRENOM=#نام#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE QZXW2HTMLCSuqZQX=#id# و 
        VERSION=#نسخه#</update>
    <!--حذف یک شخص -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#مقدار# </حذف>
</sqlMap>
  • فایل باید دارای <sqlMap> به عنوان تگ ریشه باشد (خطوط ۷ و ۴۵)
  • خطوط ۹–۱۰: برای آسان‌تر کردن نوشتن فایل، نام مستعار (واژه‌ی هممعنی) [Personne.classe] به کلاس [istia.st.springmvc.personnes.entites.Personne] اختصاص داده شده است.
  • خطوط ۱۲–۲۱: نگاشت بین ستون‌های جدول [PERSONNES] و فیلدهای شیء [Personne] را تعریف می‌کنند.
  • خطوط ۲۳–۲۴: پرس‌وجوی SQL [select] برای بازیابی همه افراد از جدول [PERSONNES]
  • خطوط 26–27: دستور SQL [select] برای بازیابی یک شخص خاص از جدول [PERSONNES]
  • خطوط ۲۹–۳۶: دستور SQL [insert] که یک شخص را در جدول [PERSONNES] درج می‌کند
  • خطوط ۳۸–۴۱: دستور SQL [update]، که یک شخص را در جدول [PERSONNES] به‌روزرسانی می‌کند
  • خطوط ۴۲–۴۴: دستور SQL [delete]، که یک شخص را از جدول [PERSONNES] حذف می‌کند

نقش و اهمیت محتوای فایل [personnes-firebird.xml] با بررسی کلاس [DaoImplCommon] که لایه [dao] را پیاده‌سازی می‌کند، توضیح داده خواهد شد.

17.3.3. کلاس [DaoImplCommon]

بیایید به معماری دسترسی به داده‌ها بازگردیم:

کلاس [DaoImplCommon] به شرح زیر است:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

     // فهرست افراد
    public Collection getAll() {
...
    }

     //بازیابی یک شخص خاص
    public Personne getOne(int id) {
...
    }

     // حذف یک شخص
    public void deleteOne(int id) {
...
    }

     // افزودن یا ویرایش یک شخص
    public void saveOne(Personne personne) {
         //آیا پارامتر person معتبر است؟
        check(personne);
         // افزودن یا ویرایش؟
        if (personne.getId() == -1) {
             // افزودن
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

     // افزودن یک شخص
    protected void insertPersonne(Personne personne) {
...
    }

     // ویرایش یک شخص
    protected void updatePersonne(Personne personne) {
...
    }

     //بررسی اعتبار یک شخص
    private void check(Personne p) {
...
    }

...
}

ما متدها را یکی یکی بررسی خواهیم کرد.


getAll


این متد تمام افراد موجود در لیست را بازیابی می‌کند. کد آن به شرح زیر است:

1
2
3
4
     // فهرست افراد
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}

ابتدا بیایید به یاد داشته باشیم که کلاس [DaoImplCommon] از کلاس Spring یعنی [SqlMapClientDaoSupport] ارث می‌برد. این کلاس حاوی متد [getSqlMapClientTemplate()] است که در خط ۳ بالا استفاده شده است. این متد امضای زیر را دارد:

Image

نوع [SqlMapClientTemplate] شیء [SqlMapClient] را از لایه [iBATIS] در بر می‌گیرد. این از طریق همین نوع است که ما به پایگاه داده دسترسی پیدا خواهیم کرد. نوع [iBATIS] SqlMapClient می‌تواند مستقیماً استفاده شود، زیرا کلاس [SqlMapClientDaoSupport] به آن دسترسی دارد:

Image

نقطه ضعف کلاس [iBATIS] SqlMapClient این است که استثناءهایی از نوع [SQLException]، یک نوع استثناء کنترل‌شده، c.a.d، را پرتاب می‌کند. این مورد باید توسط یک بلاک try/catch مدیریت شود یا در امضای متدهایی که آن را پرتاب می‌کنند، اعلام گردد. با این حال، باید به خاطر داشته باشیم که لایه [dao] یک اینترفیس [IDao] را پیاده‌سازی می‌کند که متدهای آن در امضاهای خود هیچ استثنایی ندارند. در نتیجه، متدهای کلاس‌هایی که رابط [IDao] را پیاده‌سازی می‌کنند نیز نمی‌توانند در امضاهای خود استثناء داشته باشند. بنابراین، ما باید هر استثنای [SQLException] را که توسط لایه [iBATIS] پرتاب می‌شود، دریافت (catch) کرده و آن را در یک استثنای رسیدگی‌نشده (unhandled exception) جای دهیم. نوع [DaoException] از پروژه ما برای این جای‌گذاری مناسب خواهد بود.

به جای رسیدگی به این استثناءها به صورت دستی، آن‌ها را به نوع Spring با نام [SqlMapClientTemplate] واگذار خواهیم کرد که شیء [SqlMapClient] را از لایه [iBATIS] در بر می‌گیرد. در واقع، [SqlMapClientTemplate] برای رهگیری استثناءهای [SQLException] که توسط لایه [SqlMapClient] پرتاب می‌شوند، و محصور کردن آن‌ها در یک نوع [DataAccessException] از نوع un d طراحی شده است. این رفتار با نیازهای ما سازگار است. ما صرفاً باید به خاطر داشته باشیم که لایه [dao] اکنون قادر است دو نوع استثنای گرفته‌نشده را پرتاب کند:

  • نوع سفارشی ما [DaoException]
  • نوع Spring [DataAccessException]

نوع [SqlMapClientTemplate] به شرح زیر تعریف شده است:

Image

این کلاس رابط [SqlMapClientOperations] زیر را پیاده‌سازی می‌کند:

Image

این رابط متدهایی را تعریف می‌کند که قادر به استفاده از محتویات فایل [personnes-firebird.xml] هستند:

[queryForList]

Image

این متد به شما امکان می‌دهد تا یک فرمان [SELECT] را صادر کرده و نتیجه را به صورت یک لیست از اشیاء بازیابی کنید:

  • [statementName]: شناسه‌ی (id) دستور [select] در فایل پیکربندی
  • [parameterObject]: شیء «پارامتر» برای یک [select] پیکربندی‌شده. شیء «پارامتر» می‌تواند دو شکل داشته باشد:
    • یک شیء منطبق با استاندارد JavaBean: پارامترهای دستور [select] در این صورت نام فیلدهای JavaBean خواهند بود. هنگامی که دستور [select] اجرا می‌شود، این نام‌ها با مقادیر این فیلدها جایگزین می‌شوند.
    • یک دیکشنری: پارامترهای فرمان [select] در این صورت کلیدهای دیکشنری هستند. وقتی فرمان [select] اجرا می‌شود، این‌ها با مقادیر متناظرشان در دیکشنری جایگزین می‌شوند.
  • اگر [SELECT] هیچ ردیفی بازنمی‌گرداند، نتیجه [List] یک شیء خالی است، اما null نیست (برای تأیید).

[queryForObject]

Image

این روش از نظر اصول مشابه روش قبلی است، اما تنها یک شیء را برمی‌گرداند. اگر [SELECT] هیچ ردیفی را برنگرداند، نتیجه، نشانگر null است.

[insert]

Image

این متد به شما امکان می‌دهد تا یک دستور SQL [insert] را که توسط پارامتر دوم پیکربندی شده است، اجرا کنید. شیء بازگشتی، کلید اصلی ردیفی است که درج شده است. استفاده از این نتیجه الزامی نیست.

[update]

Image

این متد یک دستور SQL [update] را که توسط پارامتر دوم پیکربندی شده است، اجرا می‌کند. نتیجه، تعداد ردیف‌هایی است که توسط دستور SQL [update] تغییر یافته‌اند.

[delete]

Image

این متد فرمان SQL [delete] را که توسط پارامتر دوم پیکربندی شده است اجرا می‌کند. نتیجه، تعداد سطرهایی است که توسط فرمان SQL [delete] حذف شده‌اند.

بیایید به متد [getAll] کلاس [DaoImplCommon] بازگردیم:

1
2
3
4
     // فهرست افراد
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}
  • خط ۴: دستور [select] با نام «Personne.getAll» اجرا می‌شود. این دستور هیچ پارامتری ندارد، بنابراین شیء «parameter» همان null است.

در [personnes-firebird.xml]، فرمان [select]، با نام «Personne.getAll»، به شرح زیر است:


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

<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">

<sqlMap>
    <!-- کلاس مستعار [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- جدول نگاشت [PERSONNES] - شیء [Personne] -->
    <resultMap id="Personne.map" 
        class="Personne.classe">
        <result property="id" column="ID" />
        <result property="version" column="VERSION" />
        <result property="nom" column="NOM"/>
        <result property="prenom" column="PRENOM"/>
        <result property="dateNaissance" column="DATENAISSANCE"/>
        <result property="marie" column="MARIE"/>
        <result property="nbEnfants" column="NBENFANTS"/>
    </resultMap>
    <!--فهرست همه افراد -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
...
</sqlMap>
  • خط ۲۳: دستور SQL «Personne.getAll» هیچ پارامتری ندارد (در متن پرس‌وجو هیچ پارامتری وجود ندارد).
  • خط ۳ از متد [getAll]، اجرای پرس‌وجوی [select] با نام «Personne.getAll» را فراخوانی می‌کند. این پرس‌وجو اجرا خواهد شد. [iBATIS] به JDBC وابسته است. بنابراین می‌دانیم که نتیجه پرس‌وجو به شکل یک شیء [ResultSet] بازگردانده خواهد شد. در خط ۲۳، ویژگی [resultMap] تگ <select> به [iBATIS] می‌گوید که کدام «resultMap " که باید برای تبدیل هر خط از شیء [ResultSet] به‌دست‌آمده استفاده کند. این «resultMap» [Personne.map] است که در خطوط ۱۲–۲۱ تعریف شده و مشخص می‌کند چگونه یک سطر از جدول [PERSONNES] به یک شی از نوع [Personne] نگاشت شود. [iBATIS] از این نگاشت‌ها استفاده خواهد کرد تا فهرستی از اشیاء [Personne] را بر اساس سطرهای شیء [ResultSet] فراهم کند.
  • خط ۳ متد [getAll] سپس یک مجموعه از اشیاء [Personne] را بازمی‌گرداند
  • روش [queryForList] ممکن است یک استثنای Spring [DataAccessException] را پرتاب کند. ما اجازه می‌دهیم این استثنا منتقل شود.

ما سایر متدهای کلاس [AbstractDaoImpl] را مختصرتر توضیح خواهیم داد، زیرا نکات اصلی مربوط به استفاده از [iBATIS] قبلاً در بحث متد [getAll] پوشش داده شده‌اند.


getOne


این متد به شما امکان می‌دهد فردی را که با [id] شناسایی شده است بازیابی کنید. کد به شرح زیر است:

         //بازیابی یک شخص خاص
    public Personne getOne(int id) {
         //بازیابی آن از BD
        Personne personne = (Personne) getSqlMapClientTemplate()
                .queryForObject("Personne.getOne", new Integer(id));
         //آیا چیزی بازیابی شده است؟
        if (personne == null) {
             //یک استثنا پرتاب می‌شود
            throw new DaoException(
                    "La personne d'id [" + id + "] n'existe pas", 2);
        }
         // شخص را بازگردانید
        return personne;
    }
  • خط ۴: اجرای فرمان [select] با نام "Personne.getOne" را درخواست می‌کند. این در فایل [personnes-firebird.xml] به شرح زیر است:

<!-- یک شخص خاص را بازیابی می‌کند -->
        <select id="Personne.getOne" resultMap="Personne.map" parameterClass="int">
            select ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM 
            PERSONNES WHERE ID=#value#</select>

دستور SQL توسط پارامتر #value# (خط ۴) پیکربندی می‌شود. ویژگی #value# مقدار پارامتری را که به فرمان SQL ارسال می‌شود، مشخص می‌کند، زمانی که این پارامتر از نوع ساده باشد: عدد صحیح، ممیز شناور، رشته و غیره. در ویژگی‌های تگ <select>، ویژگی [parameterClass] نشان می‌دهد که پارامتر از نوع عدد صحیح است (خط ۲). در خط ۵ از [getOne] می‌بینیم که این پارامتر شناسه فردی است که در حال جستجو است، به شکل یک شیء Integer. این تغییر نوع الزامی است، زیرا پارامتر دوم [queryForList] باید از نوع [Object] باشد.

نتیجهٔ پرس‌وجوی [select] باید با استفاده از ویژگی [resultMap="Personne.map"] (خط ۲) به یک شیء تبدیل شود. بنابراین این کار نوعی از نوع [Personne] را بازمی‌گرداند.

  • خطوط ۷–۱۱: اگر پرس‌وجوی [select] هیچ ردیفی را بازنگرداند، نشانگر null از خط ۴ بازیابی می‌شود. این بدان معناست که فرد مورد جستجو پیدا نشده است. در این حالت، یک پرس‌وجوی [DaoException] با کد ۲ اجرا می‌شود (خطوط ۹–۱۰).
  • خط ۱۳: اگر هیچ استثنایی رخ نداده باشد، شیء درخواستی [Personne] بازگردانده می‌شود.

deleteOne


این روش برای حذف فردی است که با [id] شناسایی شده است. کد آن به شرح زیر است:

     // حذف یک شخص
    public void deleteOne(int id) {
         // فرد حذف می‌شود
        int n = getSqlMapClientTemplate().delete("Personne.deleteOne",
                new Integer(id));
         // آیا موفقیت‌آمیز بود
        if (n == 0) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }
  • خطوط ۴–۵: اجرای دستور [delete] با نام «Personne.deleteOne» را درخواست می‌کند. این در فایل [personnes-firebird.xml] به شرح زیر است:

<!-- حذف یک شخص -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#مقدار# </حذف>

دستور SQL توسط پارامتر #value# (خط ۳) از نوع [parameterClass="int"] (خط ۲) پیکربندی شده است. این شناسه (ID) شخص مورد جستجو خواهد بود (خط ۵ از deleteOne).

  • خط ۴: نتیجه متد delete در [SqlMapClientTemplate]، تعداد ردیف‌های حذف‌شده است.
  • خطوط ۷–۸: اگر پرس‌وجوی [delete] هیچ ردیفی را حذف نکرده باشد، این بدان معناست که شخص وجود ندارد. سپس یک پرس‌وجوی [DaoException] با کد ۲ اجرا می‌شود (خط ۸).

saveOne


این روش به شما امکان می‌دهد یک شخص جدید اضافه کنید یا شخص موجود را ویرایش کنید. کد آن به شرح زیر است:

         // افزودن یا ویرایش یک شخص
    public void saveOne(Personne personne) {
         //آیا پارامتر 'person' معتبر است؟
        check(personne);
         // افزودن یا ویرایش؟
        if (personne.getId() == -1) {
             // افزودن
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }
...
  • خط ۴: اعتبار شخص با استفاده از روش [check] بررسی می‌شود. این روش در نسخه قبلی نیز وجود داشت و در آن زمان غیرفعال شده بود. اگر شخص معتبر نباشد، یک [DaoException] را فعال می‌کند. ما اجازه می‌دهیم این خطا به مراحل بعدی ارسال شود.
  • خط ۶: اگر به این نقطه برسیم، یعنی هیچ استثنایی رخ نداده است. بنابراین شخص معتبر است.
  • خطوط ۶–۱۱: بسته به شناسه شخص، این یا یک افزودن (ID = -1) است یا یک به‌روزرسانی (ID ≠ -1). در هر دو مورد، دو متد داخلی کلاس فراخوانی می‌شوند:
    • insertPersonne: برای افزودن
    • updatePersonne: برای به‌روزرسانی

insertPersonne


این متد برای افزودن یک شخص جدید استفاده می‌شود. کد آن به شرح زیر است:

// افزودن یک شخص
    protected void insertPersonne(Personne personne) {
         // نسخهٔ اول
        personne.setVersion(1);
         // انتظار ۱۰ میلی‌ثانیه  برای اهداف آزمایشی، به جای 'false' روی 'true' تنظیم کنید
        if (true)
            wait(10);
         // وارد کردن شخص جدید در جدول BD
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • خط ۴: شماره نسخه شخص در حال ایجاد را روی ۱ تنظیم کنید
  • خط ۹: رکورد را با استفاده از پرس‌وجوی با نام "Personne.insertOne" درج کنید که به شرح زیر است:

        <insert id="Personne.insertOne" parameterClass="Personne.classe">
            <selectKey keyProperty="id">
                SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
            </selectKey>         
        insert into 
        PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#id#, #version#, #surname#, #firstname#, #dateNaissance#, #marie#, 
    #nbEnfants#) </insert>

این یک پرس‌وجوی پارامتریک است و پارامتر از نوع [Personne] است (parameterClass="Personne.classe", خط 1). fields شیء [Personne] که به‌عنوان پارامتر ارسال شده است (خط 9 از insertPersonne) برای پر کردن ستون‌های ردیفی که قرار است در جدول [PERSONNES] درج شود (خطوط 5–8) استفاده می‌شوند. یک مشکل وجود دارد که باید حل شود. در حین درج، شیء [Personne] که قرار است درج شود، دارای شناسهٔ برابر با -1 است. این مقدار باید با یک کلید اصلی معتبر جایگزین شود. برای این کار، از خطوط 2–4 تگ <selectKey> بالا استفاده می‌کنیم. آنها مشخص می‌کنند:

  • (ادامه)
    • پرس‌وجوی SQL که باید برای به‌دست‌آوردن یک مقدار کلید اصلی اجرا شود. پرس‌وجوی نشان‌داده‌شده در اینجا همان پرس‌وجویی است که در بخش 17.1 ارائه کردیم. دو نکته قابل توجه است:
      • استفاده از «as "valueالزامی است. می‌توان «as value» را نیز نوشت، اما value یک کلمه کلیدی Firebird است که باید در گیومه قرار گیرد.
      • جدول Firebird در واقع [RDB$DATABASE] نامیده می‌شود. با این حال، کاراکتر $ به عنوان [iBATIS] تفسیر می‌شود. این کار با کپی کردن آن محافظت شده است.
    • میدان شیء [Personne] که باید با مقداری که توسط دستور [SELECT] بازیابی می‌شود، مقداردهی اولیه شود، در این مورد میدان [id] است. این ویژگی [keyProperty] در خط ۲ است که این فیلد را مشخص می‌کند.
  • خطوط ۶–۷: برای اهداف آزمایشی، باید قبل از درج، ۱۰ میلی‌ثانیه صبر کنیم تا هرگونه تداخل بین نخ‌هایی را که ممکن است همزمان در حال افزودن باشند، بررسی کنیم.

updatePersonne


این روش به شما امکان می‌دهد فردی را که از قبل در جدول [PERSONNES] وجود دارد، اصلاح کنید. کد آن به شرح زیر است:

//ویرایش یک شخص
    protected void updatePersonne(Personne personne) {
         // ۱۰ میلی‌ثانیه صبر کنید  برای اهداف آزمایشی، به جای 'false' روی 'true' تنظیم کنید
        if (true)
            wait(10);
         // به‌روزرسانی
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • یک به‌روزرسانی ممکن است به حداقل دو دلیل ناموفق باشد:
    1. فردی که قرار است به‌روزرسانی شود وجود ندارد
    2. شخص مورد نظر برای به‌روزرسانی وجود دارد، اما رشته‌ای که قصد اصلاح آن را دارد، نسخه صحیح را ندارد
  • خطوط ۷–۸: پرس‌وجوی SQL [update] با نام «Personne.updateOne» اجرا می‌شود. این پرس‌وجو به شرح زیر است:

    <!-- به‌روزرسانی یک شخص -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#نسخه#+1, NOM=#نام خانوادگی#, PRENOM=#نام#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE QZXW2HTMLCSuqZQX=#id# و 
VERSION=#نسخه#</update>
  • (ادامه)
    • خط ۲: پرس‌وجو پیکربندی شده و یک نوع [Personne] را به عنوان پارامتر می‌پذیرد (parameterClass="Personne.classe"). این شخص مورد اصلاح است (خط ۸ – updatePersonne).
    • ما فقط می‌خواهیم شخص موجود در جدول [PERSONNES] را که دارای همان شناسه ([id]) و همان نسخه ([version]) با پارامتر است، به‌روزرسانی کنیم. به همین دلیل ما محدودیت [WHERE ID=#id# and VERSION=#version#] را داریم. اگر این شخص پیدا شود، اطلاعات او به‌روزرسانی می‌شود تا با شخص پارامتر مطابقت داشته باشد و نسخه آن ۱ واحد افزایش می‌یابد (خط ۳ بالا).
  • خط ۹: تعداد خطوط به‌روزرسانی‌شده بازیابی می‌شود.
  • خطوط ۱۰–۱۱: اگر این عدد صفر باشد، یک [DaoException] با کد ۲ فعال می‌شود که نشان می‌دهد یا شخص مورد نظر برای به‌روزرسانی وجود ندارد، یا نسخهٔ او در این فاصله تغییر کرده است.

17.4. تست‌های لایه [dao]

17.4.1. آزمون پیاده‌سازی [DaoImplCommon]

اکنون که لایه [dao] را نوشته‌ایم، قصد داریم آن را با استفاده از تست‌های JUnit آزمایش کنیم:

Image

قبل از انجام آزمایش‌های فشرده، می‌توانیم با یک برنامه ساده از نوع [main] شروع کنیم که محتویات جدول [PERSONNES] را نمایش می‌دهد. این کلاس [MainTestDaoFirebird] است:

package istia.st.mvc.personnes.tests;

import istia.st.mvc.personnes.dao.IDao;

import java.util.Collection;
import java.util.Iterator;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

public class MainTestDaoFirebird {
    public static void main(String[] args) {
        IDao dao = (IDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao-firebird.xml"))).getBean("dao");
         // فهرست فعلی
        Collection personnes = dao.getAll();
         //نمایش کنسول
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }
}

فایل پیکربندی [spring-config-test-dao-firebird.xml] برای لایه [dao]، که در خطوط ۱۳–۱۴ استفاده شده است، به شرح زیر است:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!--منبع داده DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- توجه: بین دو تگ <value> هیچ فاصله‌ای قرار ندهید -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!--SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!--کلاس دسترسی برای لایه [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>

این فایلی است که در بخش 17.3.2 مورد بحث قرار گرفته است.

برای آزمایش، نمونه SGBD Firebird راه‌اندازی می‌شود. محتویات جدول [PERSONNES] به شرح زیر است:

Image

اجرای برنامه [MainTestDaoFirebird] خروجی صفحه نمایش زیر را تولید می‌کند:

Image

ما با موفقیت فهرست افراد را به‌دست آوردیم. اکنون می‌توانیم به تست JUnit بپردازیم.

آزمون JUnit [TestDaoFirebird] به شرح زیر است:

package istia.st.mvc.personnes.tests;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Iterator;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;
import junit.framework.TestCase;

public class TestDaoFirebird extends TestCase {

     //لایه [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

     // تولیدکننده
    public void setUp() {
        dao = (IDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao-firebird.xml"))).getBean("dao");
    }

     // فهرست افراد
    private void doListe(Collection personnes) {
...
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // اصلاح/حذف یک عنصر موجود نیست
    public void test2() throws ParseException {
..
    }

     // مدیریت نسخه‌های کاربر
    public void test3() throws ParseException, InterruptedException {
...
    }

     //قفل‌گذاری خوش‌بینانه – دسترسی چندرشته‌ای
    public void test4() throws Exception {
...
    }

     //بررسی‌های اعتبار برای saveOne
    public void test5() throws ParseException {
....
    }

     //درج‌های چندرشته‌ای
    public void test6() throws ParseException, InterruptedException{
...
}
  • آزمایش‌های [test1] تا [test5] همانند نسخه ۱ هستند، به جز [test4] که کمی تغییر کرده است. آزمایش [test6] جدید است. ما فقط در مورد این دو آزمایش توضیح خواهیم داد.

[test4]


[test4] برای آزمایش روش [updatePersonne - DaoImplCommon] طراحی شده است. کد آن روش به این صورت است:

//ویرایش یک شخص
    protected void updatePersonne(Personne personne) {
         // منتظر ۱۰ میلی‌ثانیه  برای تست روی true تنظیم کنید به جای false
        if (true)
            wait(10);
         // به‌روزرسانی
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • خطوط ۴–۵: ما به مدت ۱۰ میلی‌ثانیه منتظر می‌مانیم. این کار باعث می‌شود تاثری که [updatePersonne] را اجرا می‌کند، کنترل پردازنده مرکزی (CPU) را از دست بدهد، که ممکن است شانس ما را برای مشاهده تضادهای دسترسی بین نخ‌های همزمان افزایش دهد.

[test4] N=100 نخ را راه‌اندازی می‌کند که وظیفه دارند همزمان تعداد فرزندان یک شخص را به اندازه 1 افزایش دهند. ما می‌خواهیم ببینیم چگونه تعارضات نسخه‌ای و تعارضات دسترسی مدیریت می‌شوند.

    public void test4() throws Exception {
         // افزودن یک شخص
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // ایجاد N نخ برای به‌روزرسانی تعداد فرزندان
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
         // انتظار برای پایان یافتن نخ‌ها
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // بازیابی شخص
        p1 = dao.getOne(id1);
         // آنها باید N فرزند داشته باشند
        assertEquals(N, p1.getNbEnfants());
         // حذف شخص p1
        dao.deleteOne(p1.getId());
         // بررسی
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         //باید خطای کد ۲ رخ دهد
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }

رشته‌ها در خطوط ۸–۱۳ ایجاد شده‌اند. هر یک تعداد فرزندان شخص ایجادشده در خطوط ۳–۵ را به اندازه ۱ افزایش می‌دهد. رشته‌های به‌روزرسانی [ThreadDaoMajEnfants ] به شرح زیر هستند:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoMajEnfants extends Thread {
     // نام نخ
    private String name;

     // مرجع در لایه [dao]
    private IDao dao;

     //شناسه فردی که قرار است روی او کار کنیم
    private int idPersonne;

     // سازنده
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        this.idPersonne = idPersonne;
    }

     // بدنهٔ نخ
    public void run() {
         // ردیابی
        suivi("lancé");
         // حلقه تا زمانی که با موفقیت یک واحد افزایش پیدا کند
         // تعداد فرزندان شخص idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
             // یک نسخه از شخص را از idPersonne بازیابی کنید
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
             // به دنبال
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1)
                    + " pour la version " + personne.getVersion());
             // منتظر ۱۰ میلی‌ثانیه بمانید قبل از واگذاری پردازنده
            try {
                 //پیگیری
                suivi("début attente");
                 // موقتا متوقف شدن برای واگذاری پردازنده
                Thread.sleep(10);
                 //پیگیری
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
             // انتظار کامل – در حال تلاش برای اعتبارسنجی کپی
             // در این میان، سایر نخ‌ها ممکن است نسخهٔ اصلی را تغییر داده باشند
            int codeErreur = 0;
            try {
                 // افزایش شمار فرزندان این کپی به اندازهٔ ۱
                personne.setNbEnfants(nbEnfants + 1);
                 // در حال تلاش برای تغییر نسخه اصلی
                dao.saveOne(personne);
                 // ما به سراغ چیز دیگری رفته‌ایم – نسخهٔ اصلی تغییر کرده است
                fini = true;
            } catch (DaoException ex) {
                 // کد خطا را بازیابی کنید
                codeErreur = ex.getCode();
                 // اگر خطایی از ID یا کد خطا نسخهٔ ۲ رخ دهد، به‌روزرسانی دوباره انجام می‌شود
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                     // استثنای مدیریت‌نشده – اجازه دهید منتقل شود
                    throw ex;
                }
            }
        }
         // ردیابی
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

     //پیگیری
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}

به‌روزرسانی یک شخص ممکن است به‌دلیل عدم وجود شخص مورد اصلاح یا به‌روزرسانی شدن قبلی آن توسط نخ دیگری با شکست مواجه شود. هر دو حالت در اینجا در خطوط 67–69 رسیدگی شده‌اند. در هر دو مورد، متد [updatePersonne] یک [DaoException] با کد 2 را راه‌اندازی می‌کند. سپس این نخ به شروع مجدد فرآیند به‌روزرسانی از ابتدا بازگردانده می‌شود (حلقه while، خط 34).


[test6]


[test6] برای تست متد [insertPersonne - DaoImplCommon] طراحی شده است. کد آن متد به این صورت است:

// افزودن یک شخص
    protected void insertPersonne(Personne personne) {
         // نسخهٔ اول
        personne.setVersion(1);
         // منتظر ۱۰ میلی‌ثانیه بمانید  برای تست، به جای false روی true تنظیم کنید
        if (true)
            wait(10);
         // وارد کردن شخص جدید در جدول BD
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • خطوط ۶–۷: ما ۱۰ میلی‌ثانیه صبر می‌کنیم تا نخ در حال اجرای [insertPersonneCPU را از دست بدهد و بدین ترتیب شانس مشاهده تضادهای ناشی از اجرای همزمان عملیات درج توسط نخ‌ها را افزایش دهیم.

کد برای [test6] به شرح زیر است:

     //درج‌های چندرشته‌ای
    public void test6() throws ParseException, InterruptedException{
         // ایجاد یک شخص
        Personne p = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
         //که N بار در یک آرایه کپی می‌شود
        final int N = 100;
        Personne[] personnes=new Personne[N];
        for(int i=0;i<personnes.length;i++){
            personnes[i]=new Personne(p);
        }
         //ایجاد N نخ درج – هر نخ یک شخص را درج می‌کند
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoInsertPersonne("thread n° " + i, dao, personnes[i]);
            taches[i].start();
        }
         // انتظار برای پایان یافتن نخ‌ها
        for (int i = 0; i < taches.length; i++) {
             // رشته شماره i
            taches[i].join();
             // حذف شخص
            dao.deleteOne(personnes[i].getId());
        }
}

ما ۱۰۰ نخ ایجاد می‌کنیم که به‌طور همزمان ۱۰۰ شخص مختلف را درج خواهند کرد. این ۱۰۰ نخ همگی یک کلید اصلی برای شخصی که قرار است درج کنند دریافت می‌کنند، سپس قبل از اینکه بتوانند درج خود را انجام دهند، به مدت ۱۰ میلی‌ثانیه معلق می‌شوند (خط ۱۰ – insertPersonne). ما می‌خواهیم بررسی کنیم که همه چیز به درستی کار می‌کند و به خصوص اینکه آن‌ها واقعاً مقادیر کلید اصلی متفاوتی دریافت می‌کنند.

  • خطوط ۷–۱۱: یک آرایه از ۱۰۰ نفر ایجاد می‌شود. این افراد همگی کپی‌هایی از شخص «p» هستند که در خطوط ۴–۵ ایجاد شده است.
  • خطوط 14–17: 100 نخ درج راه‌اندازی می‌شوند. هر یک مأمور درج یکی از 100 نفری هستند که قبلاً ایجاد شده‌اند.
  • خطوط ۱۹–۲۳: [test6] منتظر پایان کار هر یک از ۱۰۰ رشته‌ای که راه‌اندازی کرده است، می‌ماند. وقتی تشخیص می‌دهد که رشته با شماره i کارش تمام شده است، فردی را که این رشته به تازگی وارد کرده است، حذف می‌کند.

رشته درج [ThreadDaoInsertPersonne] به شرح زیر است:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoInsertPersonne extends Thread {
     // نام نخ
    private String name;

     // مرجع در لایه [dao]
    private IDao dao;

     //شناسهٔ فردی که باید روی او کار شود
    private Personne personne;

     // آفریننده
    public ThreadDaoInsertPersonne(String name, IDao dao, Personne personne) {
        this.name = name;
        this.dao = dao;
        this.personne = personne;
    }

     // بدنهٔ نخ
    public void run() {
         // ردیابی
        suivi("lancé");
         // وارد کردن
        dao.saveOne(personne);
         // ردیابی
        suivi("a terminé");
    }

     // ردیابی
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}
  • خطوط ۱۹–۲۲: سازندهٔ نخ، شخص مورد نیاز برای درج و لایهٔ [dao] را که برای انجام این درج باید استفاده کند، ذخیره می‌کند.
  • خط ۳۰: شخص درج می‌شود. اگر استثنا رخ دهد، به [test6] منتقل می‌شود.

آزمون‌ها


در طول آزمایش، نتایج زیر به دست آمد:

بنابراین تست [test4] شکست می‌خورد. تعداد فرزندان به جای ۱۰۰ مورد انتظار به ۶۹ کاهش یافته است. چه اتفاقی افتاده است؟ بیایید لاگ‌های صفحه را بررسی کنیم. آن‌ها نشان می‌دهند که استثناها توسط Firebird پرتاب شده‌اند:


Exception in thread "Thread-62" org.springframework.jdbc.UncategorizedSQLException: SqlMapClient operation; uncategorized SQLException for SQL []; SQL state [HY000]; error code [335544336];   
--- خطا در personnes-firebird.xml رخ داد.  
--- هنگام اعمال نقشه پارامتر خطایی رخ داد.  
--- عبارت را بررسی کنید (به‌روزرسانی ناموفق بود).  
--- عبارت را بررسی کنید (به‌روزرسانی ناموفق بود).  
--- علت: org.firebirdsql.jdbc.FBSQLException: استثنای GDS. 335544336. بن‌بست
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:   
--- خطا در personnes-firebird.xml رخ داد.  
--- خطا هنگام اعمال نقشه پارامتر رخ داد.  
  • خط ۱ – یک استثنای Spring از نوع [org.springframework.jdbc.UncategorizedSQLException] پرتاب شد. این یک استثنای گرفته‌نشده است که برای پوشش استثنای پرتاب‌شده توسط درایور Firebird JDBC استفاده شده است، همان‌طور که در خط ۶ توضیح داده شده است.
  • خط ۶ – درایور Firebird JDBC استثناء‌ای از نوع [org.firebirdsql.jdbc.FBSQLException] با کد خطا 335544336 پرتاب کرد.
  • خط ۷: نشان می‌دهد که یک وضعیت رقابت بین دو نخ که همزمان در حال تلاش برای به‌روزرسانی همان سطر در جدول [PERSONNES] بودند، وجود داشته است.

این یک خطای مرگبار نیست. تریدی که این استثنا را می‌گیرد می‌تواند عملیات به‌روزرسانی را مجدداً امتحان کند. برای این کار، کد مربوط به [ThreadDaoMajEnfants] باید اصلاح شود:

            try {
                 // تعداد فرزندان این کپی را ۱ افزایش میدهد
                personne.setNbEnfants(nbEnfants + 1);
                 // در حال تلاش برای تغییر نسخه اصلی است
                dao.saveOne(personne);
                 // موفقیتآمیز  نسخهٔ اصلی اصلاح شد
                fini = true;
            } catch (DaoException ex) {
                 // در حال بازیابی کد خطا
                codeErreur = ex.getCode();
                 // اگر خطایی با ID یا خطای نسخه کد ۲ رخ دهد، بهروزرسانی مجدداً امتحان میشود
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                     // استثناء کنترلنشده  اجازه دهید منتقل شود
                    throw ex;
                }
  • خط ۸: یک استثنا از نوع [DaoException] مدیریت می‌شود. بر اساس آنچه گفته شد، ما باید استثنایی را که در حین آزمایش رخ داده است، یعنی نوع [org.springframework.jdbc.UncategorizedSQLException]، مدیریت کنیم. با این حال، ما نمی‌توانیم به سادگی این نوع را مدیریت کنیم، زیرا این یک نوع عمومی Spring است که برای دربرگیری استثناهایی طراحی شده که آن را تشخیص نمی‌دهد. اسپرینگ استثناهای پرتاب‌شده توسط درایورهای JDBC را برای تعدادی از سیستم‌های SGBD مانند Oracle، MySQL، تشخیص می‌دهد، Postgres، DB2، SQL Server، … اما نه Firebird. بنابراین، هر استثنایی که توسط درایور Firebird JDBC پرتاب می‌شود، در نوع Spring [org.springframework.jdbc.UncategorizedSQLException] پیچیده می‌شود:

Image

همانطور که در بالا مشاهده می‌شود، کلاس [UncategorizedSQLException] از کلاس [DataAccessException] که در بخش 17.3.3 به آن پرداختیم، ارث می‌برد. امکان شناسایی استثنایی که در [UncategorizedSQLException] محصور شده است، از طریق متد [getSQLException] آن وجود دارد:

Image

این استثنا از نوع [SQLException] همان است که توسط لایه [iBATIS] پرتاب می‌شود، که خود استثنای پرتاب‌شده توسط درایور پایگاه داده JDBC را در بر می‌گیرد. علت دقیق استثنای [SQLException] را می‌توان با استفاده از روش زیر به‌دست آورد:

Image

این شیء از نوع [Throwable] را بازمی‌گرداند که توسط درایور JDBC پرتاب شده است:

Image

نوع [Throwable] کلاس والد [Exception] است.

در اینجا، باید تأیید کنیم که شیء از نوع [Throwable]، که توسط درایور Firebird JDBC پرتاب شده و علت ...استثنای [SQLException] که توسط لایه [iBATIS] پرتاب شده است، در واقع یک استثنا از نوع [org.firebirdsql.gds.GDSException] با کد خطای 335544336 است. برای بازیابی کد خطا، می‌توانیم از متد [getErrorCode()] کلاس [org.firebirdsql.gds.GDSException] استفاده کنیم.

اگر از استثنای [org.firebirdsql.gds.GDSException] در کد [ThreadDaoMajEnfants] استفاده کنیم، آنگاه این نخ تنها قادر خواهد بود با نمونه Firebird از کلاس SGBD کار کند. همین امر در مورد تست [test4] که از این نخ استفاده می‌کند نیز صدق می‌کند. ما می‌خواهیم از این امر اجتناب کنیم. در واقع، ما می‌خواهیم تست‌های JUnit ما صرف‌نظر از اینکه کدام SGBD استفاده می‌شود، معتبر باقی بمانند. برای دستیابی به این هدف، ما تصمیم گرفته‌ایم که لایه [dao] هرگاه استثنای «تضاد به‌روزرسانی» تشخیص داده شود، صرف‌نظر از SGBD زیرین، یک [DaoException] با کد ۴ را راه‌اندازی کند. بنابراین، رشته [ThreadDaoMajEnfants] را می‌توان به صورت زیر بازنویسی کرد:

package istia.st.mvc.personnes.tests;
...

public class ThreadDaoMajEnfants extends Thread {
...

     //هستهٔ نخ
    public void run() {
...
        while (!fini) {
             // یک نسخه از شخص را از idPersonne بازیابی کنید
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
...
             // انتظار کامل – تلاش برای اعتبارسنجی کپی
             // در این میان، ممکن است رشته‌های دیگر نسخهٔ اصلی را تغییر داده باشند
            int codeErreur = 0;
            try {
                 // افزایش شمار فرزندان این کپی به اندازهٔ ۱
                personne.setNbEnfants(nbEnfants + 1);
                 // در حال تلاش برای تغییر نسخهٔ اصلی
                dao.saveOne(personne);
                 //پردازش شده است – نسخهٔ اصلی اصلاح شده است
                fini = true;
            } catch (DaoException ex) {
                 //کد خطا را بازیابی کنید
                codeErreur = ex.getCode();
                 //اگر خطایی از نوع ID یا نسخهٔ ۲، یا بن‌بست ۴ رخ دهد، آنگاه
                 // دوباره تلاش برای به‌روزرسانی
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                case 4:
                    suivi("conflit de mise à jour");
                    break;
                default:
                     // استثنای مدیریت‌نشده – اجازه دهید منتقل شود
                    throw ex;
                }
            }
        }
         //پیگیری
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }
...
}
  • خطوط ۳۴–۳۶: استثنای [DaoException] با کد ۴ رهگیری می‌شود. نخ [ThreadDaoMajEnfants] مجبور به راه‌اندازی مجدد فرآیند به‌روزرسانی از ابتدا (خط ۱۰) خواهد شد.

لایه [dao] ما باید قادر باشد استثناء از نوع «تضاد به‌روزرسانی» را تشخیص دهد. این استثناء توسط یک درایور JDBC ایجاد می‌شود و مختص آن است. این استثنا باید در متد [updatePersonne] از کلاس [DaoImplCommon] مدیریت شود:

//ویرایش یک شخص
    protected void updatePersonne(Personne personne) {
         // منتظر ۱۰ میلی‌ثانیه بمانید  برای تست، به جای false روی true تنظیم کنید
        if (true)
            wait(10);
         // به‌روزرسانی
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }

خطوط ۷ تا ۱۱ باید در داخل یک بلوک try/catch قرار گیرند. برای درایور SGBD Firebird، باید بررسی کنیم که استثنایی که باعث شکست به‌روزرسانی شده، از نوع [org.firebirdsql.gds.GDSException] بوده و کد خطای 335544336 را داشته باشد. اگر این نوع تست را در [DaoImplCommon] قرار دهیم، این کلاس را به کلاس SGBD Firebird لینک خواهیم کرد که واضحاً نامطلوب است. اگر بخواهیم کلاس [DaoImplCommon] را با هدف عمومی حفظ کنیم، باید از آن ارث ببریم و مدیریت استثنا را در یک کلاس اختصاصی Firebird انجام دهیم. کاری که اکنون در حال انجام آن هستیم همین است.

17.4.2. کلاس [DaoImplFirebird]

کد آن به شرح زیر است:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;

public class DaoImplFirebird extends DaoImplCommon {

     // ویرایش یک شخص
    protected void updatePersonne(Personne personne) {
         // منتظر ۱۰ میلی‌ثانیه – برای تست، مقدار را روی 'true' به جای 'false' تنظیم کنید
        if (true)
            wait(10);
         // اصلاح
        try {
             // به‌روزرسانی شخص با نسخه صحیح
            int n = getSqlMapClientTemplate().update("Personne.updateOne",
                    personne);
            if (n == 0)
                throw new DaoException("La personne d'Id [" + personne.getId()
                        + "] n'existe pas ou bien a été modifiée", 2);
        } catch (org.springframework.jdbc.UncategorizedSQLException ex) {
            if (ex.getSQLException().getCause().getClass().isAssignableFrom(
                    org.firebirdsql.jdbc.FBSQLException.class)) {
                org.firebirdsql.jdbc.FBSQLException cause = (org.firebirdsql.jdbc.FBSQLException) ex
                        .getSQLException().getCause();
                if (cause.getErrorCode() == 335544336) {
                    throw new DaoException(
                            "Conflit d'accès au même enregistrement", 4);
                }
            } else {
                throw ex;
            }
        }
    }

     // انتظار
    private void wait(int N) {
         // منتظر N میلی‌ثانیه
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
             // نمایش ردیابی استثنا
            e.printStackTrace();
            return;
        }
    }

}
  • خط ۵: کلاس [DaoImplFirebird] از کلاس [DaoImplCommon] که همین‌الان بررسی کردیم، ارث می‌برد. این کلاس در خطوط ۸ تا ۳۳، متد [updatePersonne] را که برای ما مشکل ایجاد می‌کند، بازتعریف می‌کند.
  • خط ۲۰: استثناء Spring از نوع [UncategorizedSQLException] را می‌گیریم
  • خطوط ۲۱–۲۲: ما بررسی می‌کنیم که استثنای زمینه‌ای از نوع [SQLException]، که توسط لایه [iBATIS] پرتاب شده است، ناشی از یک استثنا از نوع [org.firebirdsql.jdbc.FBSQLException] است.
  • خط ۲۵: همچنین بررسی می‌کنیم که کد خطا برای این استثنای Firebird برابر با 335544336، یعنی کد خطای «بن‌بست» (deadlock)، است.
  • خطوط ۲۶–۲۷: اگر همه این شرایط برآورده شوند، یک [DaoException] با کد ۴ ایجاد می‌شود.
  • خطوط ۳۶–۴۴: متد [wait] نخ جاری را برای N میلی‌ثانیه متوقف می‌کند. این تنها برای اهداف تست مفید است.

اکنون آمادهٔ آزمایش لایهٔ جدید [dao] هستیم.

17.4.3. آزمایش پیاده‌سازی [DaoImplFirebird]

فایل پیکربندی تست [spring-config-test-dao-firebird.xml] برای استفاده از پیاده‌سازی [DaoImplFirebird] اصلاح می‌شود:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!--منبع داده DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- هشدار: بین دو تگ <value> هیچ فاصله‌ای نگذارید -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!--کلاس دسترسی برای لایه [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • خط ۳۲: پیاده‌سازی جدید [DaoImplFirebird] از لایه [dao].

نتایج آزمون [test4] که قبلاً ناموفق بود، به شرح زیر است:

Image

[test4] با موفقیت انجام شد. خطوط پایانی گزارش‌های صفحه به شرح زیر است:

1
2
3
4
5
6
7
thread n° 36 [1145977145984] : fin attente
thread n° 75 [1145977145984] : a terminé et passé le nombre d'enfants à 99
thread n° 36 [1145977146000] : version corrompue ou personne inexistante
thread n° 36 [1145977146000] : 99 -> 100 pour la version 100
thread n° 36 [1145977146000] : début attente
thread n° 36 [1145977146015] : fin attente
thread n° 36 [1145977146031] : a terminé et passé le nombre d'enfants à 100

خط آخر نشان می‌دهد که رشته شماره ۳۶ آخرین رشته‌ای بود که به پایان رسید. خط ۳ یک تداخل نسخه‌ای را نشان می‌دهد که رشته شماره ۳۶ را مجبور به راه‌اندازی مجدد رویه به‌روزرسانی افراد کرد (خط ۴). سایر لاگ‌ها تداخل‌های دسترسی را در حین به‌روزرسانی‌ها نشان می‌دهند:

1
2
3
thread n° 52 [1145977145765] : version corrompue ou personne inexistante
thread n° 75 [1145977145765] : conflit de mise à jour
thread n° 36 [1145977145765] : version corrompue ou personne inexistante

خط ۲ نشان می‌دهد که نخ شماره ۷۵ در حین به‌روزرسانی خود به دلیل تداخل به‌روزرسانی با شکست مواجه شد: هنگامی که دستور SQL [update] بر روی جدول [PERSONNES] صادر شد، ردیفی که نیاز به به‌روزرسانی داشت توسط یک نخ دیگر قفل شده بود. این تداخل دسترسی، نخ شماره ۷۵ را مجبور به تلاش مجدد برای به‌روزرسانی خود می‌کند.

در نهایت، در مورد [test4]، تفاوت قابل توجهی با نتایج همان تست در نسخه ۱ وجود دارد که در آن به دلیل مشکلات همگام‌سازی ناموفق بود. از آنجایی که متدها در لایه [dao] نسخه ۱ همگام‌سازی نشده بودند، تعارضات دسترسی به وجود آمد. در اینجا، ما نیازی به همگام‌سازی لایه [dao] نداشتیم. ما به سادگی با تعارضات دسترسی که توسط Firebird گزارش می‌شد، برخورد کردیم.

اکنون بیایید کل تست JUnit را برای لایه [dao] اجرا کنیم:

Image

بنابراین به نظر می‌رسد که ما یک لایه معتبر [dao] داریم. برای اعلام اعتبار آن با اطمینان بالا، نیاز به انجام آزمایش‌های بیشتری داریم. با این حال، ما آن را عملیاتی در نظر خواهیم گرفت.

17.5. لایه [service]

17.5.1. اجزای لایه [service]

لایه [service] شامل کلاس‌ها و رابط‌های زیر است:

Image

  • [IService] رابطی است که توسط لایه [service] ارائه می‌شود
  • [ServiceImpl] پیاده‌سازی این است

رابط [IService] به شرح زیر است:

package istia.st.mvc.personnes.service;

import istia.st.mvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
     // فهرست همه افراد
    Collection getAll();

     //بازیابی یک شخص خاص
    Personne getOne(int id);

     // افزودن/ویرایش یک شخص
    void saveOne(Personne personne);

     // حذف یک شخص
    void deleteOne(int id);

     // ذخیرهٔ چند نفر
    void saveMany(Personne[] personnes);

     // حذف چند نفر
    void deleteMany(int ids[]);
}
  • این رابط همان چهار متد نسخهٔ ۱ را دارد، اما دو متد اضافی نیز دارد:
    • saveMany: امکان ذخیرهٔ چند نفر را به‌طور اتمیک هم‌زمان فراهم می‌کند. یا همگی ذخیره می‌شوند یا هیچ‌کدام ذخیره نمی‌شوند.
    • deleteMany: به شما امکان می‌دهد چندین نفر را به طور همزمان به صورت اتمی حذف کنید. یا همگی حذف می‌شوند، یا هیچ‌کدام حذف نمی‌شوند.

این دو متد توسط وب‌اپلیکیشن استفاده نخواهند شد. ما آن‌ها را برای تشریح مفهوم تراکنش پایگاه داده اضافه کرده‌ایم. در واقع، هر دو متد باید در داخل یک تراکنش اجرا شوند تا به یکپارچگی اتمی مورد نظر دست یابند.

کلاس [ServiceImpl] که این رابط را پیاده‌سازی می‌کند، به شکل زیر خواهد بود:

package istia.st.mvc.personnes.service;

import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.dao.IDao;

import java.util.Collection;

public class ServiceImpl implements IService {

     //لایه [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

     // فهرست افراد
    public Collection getAll() {
        return dao.getAll();
    }

     //بازیابی یک شخص خاص
    public Personne getOne(int id) {
        return dao.getOne(id);
    }

     // افزودن یا ویرایش یک شخص
    public void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

     // حذف یک شخص
    public void deleteOne(int id) {
        dao.deleteOne(id);
    }

     //ذخیرهٔ مجموعه‌ای از افراد
    public void saveMany(Personne[] personnes) {
         // پیمایش آرایه افراد
        for (int i = 0; i < personnes.length; i++) {
            dao.saveOne(personnes[i]);
        }
    }

     // حذف یک مجموعه از افراد
    public void deleteMany(int[] ids) {
         //ids: شناسه‌های افرادی که باید حذف شوند
        for (int i = 0; i < ids.length; i++) {
            dao.deleteOne(ids[i]);
        }
    }
}
  • متدهای [getAll, getOne, insertOne, saveOne] متدهای لایه [dao] را با همان نام فراخوانی می‌کنند.
  • خطوط ۴۲–۴۷: متد [saveMany] به‌صورت تک‌تک، افراد را از آرایه‌ای که به‌عنوان پارامتر ارسال شده است، ذخیره می‌کند.
  • خطوط ۵۰–۵۵: متد [deleteMany] به‌صورت جداگانه افراد را از آرایه‌ای که به‌عنوان پارامتر از id ارسال شده است، حذف می‌کند

ما بیان کرده‌ایم که متدهای [saveMany] و [deleteMany] باید در داخل یک تراکنش اجرا شوند تا ماهیت «همه یا هیچ» این متدها تضمین شود. می‌توانیم ببینیم که کد بالا این مفهوم تراکنش را به طور کامل نادیده می‌گیرد. این تنها در فایل پیکربندی لایه [service] ظاهر خواهد شد.

17.5.2. پیکربندی لایه [service]

در بالا، در خط ۱۱، می‌توانیم ببینیم که پیاده‌سازی [ServiceImpl] یک مرجع به لایه [dao] را در اختیار دارد. این لایه، همانند نسخهٔ ۱، توسط Spring زمانی که لایهٔ [service - ServiceImpl] نمونه برداری می‌شود، inicialize خواهد شد. فایل پیکربندی که نمونه برداری لایهٔ [service] را فعال می‌کند، به شرح زیر خواهد بود:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!--منبع داده DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <property name="url">
            <!-- توجه: بین دو تگ <value> هیچ فاصله‌ای قرار ندهید -->
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!--کلاس دسترسی لایه [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!--مدیر تراکنش -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!--کلاس‌های دسترسی برای لایه [service] -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
</beans>
  • خطوط ۱–۳۶: پیکربندی لایه [dao]. این پیکربندی هنگام بحث در مورد لایه [dao] در بخش 17.3.2 توضیح داده شد.
  • خطوط ۳۸–۶۴: پیکربندی لایه [service]

در خط ۴۶، می‌بینیم که لایه [service] توسط نوع [TransactionProxyFactoryBean] پیاده‌سازی شده است. ما انتظار داشتیم نوع [ServiceImpl] را بیابیم. [TransactionProxyFactoryBean] یک نوع پیش‌تعریف‌شده در Spring است. چگونه یک نوع پیش‌تعریف‌شده می‌تواند رابط [IService] را که مختص برنامه ماست پیاده‌سازی کند؟

ابتدا بیایید نگاهی به کلاس [TransactionProxyFactoryBean] بیندازیم:

Image

می‌توانیم ببینیم که این کلاس، رابط [FactoryBean] را پیاده‌سازی می‌کند. ما قبلاً با این رابط مواجه شده‌ایم. می‌دانیم که وقتی یک برنامه از Spring یک نمونه از نوعی را که [FactoryBean] را پیاده‌سازی می‌کند درخواست می‌کند، اسپرینگ نه یک نمونه [I] از آن نوع را بازمی‌گرداند، بلکه شیء بازگردانده‌شده توسط متد [I].getObject() را بازمی‌گرداند.

Image

در مورد ما، لایه [service] توسط ابجکت بازگردانده‌شده توسط [TransactionProxyFactoryBean].getObject() پیاده‌سازی خواهد شد. ماهیت این ابجکت چیست؟ ما وارد جزئیات نمی‌شویم چون پیچیده‌اند. این موارد مربوط به چیزی است که به آن Spring AOP (برنامه‌نویسی جنبه‌گرا) گفته می‌شود. سعی می‌کنیم با چند نمودار ساده موضوع را روشن کنیم. AOP موارد زیر را ممکن می‌سازد:

  • ما دو کلاس C1 و C2 داریم که C1 از رابط [I2] که توسط C2 ارائه شده است، استفاده می‌کند:
  • به لطف AOP، می‌توان یک میان‌گیر (interceptor) را بین کلاس‌های C1 و C2 قرار داد، به گونه‌ای که برای هر دو کلاس شفاف باشد:

کلاس [C1] برای کار با رابط [I2] کامپایل شده است، که توسط [C2] پیاده‌سازی شده است. در زمان اجرا، AOP کلاس [intercepteur] را بین [C1] و [C2] قرار می‌دهد. برای اینکه این امر ممکن شود، کلاس [intercepteur] باید، البته، همان رابط [I2] را که به [C1] ارائه می‌دهد، به [C2] نیز ارائه دهد.

این ممکن است برای چه کاری استفاده شود؟ مستندات Spring چند مثال ارائه می‌دهد. برای مثال، ممکن است بخواهید فراخوانی‌های متد خاصی به نام M در [C2] را ثبت کنید تا آن متد را حسابرسی کنید. در [intercepteur]، سپس متد [M] را برای انجام این لاگ‌ها می‌نویسید. فراخوانی از [C1] به [C2].M به شرح زیر انجام خواهد شد (به نمودار بالا مراجعه کنید):

  1. [C1] متد M از [C2] را فراخوانی می‌کند. در واقع، متد M از [intercepteur] فراخوانی خواهد شد. این امر زمانی امکان‌پذیر است که [C1] به یک رابط [I2] اشاره کند، نه به یک پیاده‌سازی خاص از [I2]. پس، تنها کاری که لازم است انجام شود این است که [intercepteur]، [I2] را پیاده‌سازی کند.
  2. روش M در [intercepteur] خروجی را ثبت می‌کند و روش M در [C2] را فراخوانی می‌کند که در ابتدا هدف [C1] بود.
  3. روش M در [C2] اجرا می‌شود و نتیجهٔ خود را به روش M در [intercepteur] بازمی‌گرداند، که می‌تواند اختیاریاً چیزی را به آنچه در مرحلهٔ ۲ انجام شده اضافه کند.
  4. روش M از [intercepteur] نتیجه را به روش فراخواننده [C1] بازمی‌گرداند

می‌توانیم ببینیم که متد M از [intercepteur] می‌تواند اقداماتی را هم قبل و هم بعد از فراخوانی متد M از [C2] انجام دهد. در ارتباط با [C1]، بنابراین متد M کلاس [C2] را گسترش می‌دهد. از این رو می‌توان فناوری AOP را به‌عنوان روشی برای گسترش رابط ارائه‌شده توسط یک کلاس در نظر گرفت.

این مفهوم چگونه در لایه [service] ما اعمال می‌شود؟ اگر ما لایه [service] را مستقیماً با استفاده از یک نمونه [ServiceImpl] پیاده‌سازی کنیم، معماری وب‌اپلیکیشن ما به شکل زیر خواهد بود:

اگر لایه [service] را با استفاده از یک نمونه [TransactionProxyFactoryBean] پیاده‌سازی کنیم، معماری زیر را خواهیم داشت:

می‌توان گفت که لایه [service] با دو شیء نمونه‌سازی می‌شود:

  • شیء مذکور که با نام [proxy transactionnel] شناخته می‌شود، در واقع شیء بازگشتی متد [getObject] از [TransactionProxyFactoryBean] است. این شیء به‌عنوان رابط بین لایه [service] و لایه [web] عمل خواهد کرد. از نظر طراحی، این شیء رابط [IService] را پیاده‌سازی می‌کند.
  • یک نمونه از [ServiceImpl] که همچنین رابط [IService] را پیاده‌سازی می‌کند. این تنها نمونه‌ای است که می‌داند چگونه با لایه [dao] کار کند، بنابراین ضروری است.

بیایید تصور کنیم که لایه [web] متد [saveMany] از اینترفیس [IService] را فراخوانی می‌کند. ما می‌دانیم که از نظر عملکردی، درج‌ها و به‌روزرسانی‌هایی که توسط این متد انجام می‌شوند باید در یک تراکنش صورت گیرند. یا همه آن‌ها با موفقیت انجام می‌شوند یا هیچ‌کدام اجرا نمی‌شوند. ما قبلاً متد [saveMany] از کلاس [ServiceImpl] را بررسی کرده و اشاره کردیم که این متد از تراکنش‌ها پشتیبانی نمی‌کند. متد [saveMany] از کلاس [proxy transactionnel]، متد [saveMany] از کلاس [ServiceImpl] را با مفهوم تراکنش گسترش خواهد داد. بیایید نمودار بالا را دنبال کنیم:

  1. لایه [web] متد [saveMany] از رابط [IService] را فراخوانی می‌کند.
  2. متد [saveMany] از کلاس [proxy transactionnel] اجرا می‌شود. این متد یک تراکنش را آغاز می‌کند. این لایه باید اطلاعات کافی برای انجام این کار را داشته باشد، به‌ویژه یک شیء [DataSource] برای برقراری ارتباط با SGBD. سپس متد [saveMany] از [ServiceImpl] را فراخوانی می‌کند.
  3. این متد اجرا می‌شود. این متد به طور مکرر لایه [dao] را برای انجام درج‌ها یا به‌روزرسانی‌ها فراخوانی می‌کند. دستورات SQL که در این مرحله اجرا می‌شوند، در چارچوب تراکنش آغازشده در مرحله ۲ انجام می‌گیرند.
  4. فرض کنید یکی از این عملیات با شکست مواجه شود. لایه [dao] اجازه می‌دهد استثناء تا لایه [service]، در این مورد متد [saveMany] از نمونه [ServiceImpl]، منتقل شود.
  5. این متد هیچ کاری انجام نمی‌دهد و اجازه می‌دهد استثناء تا متد [saveMany] از [proxy transactionnel] propagate شود.
  6. پس از دریافت استثنا، متد [saveMany] از کلاس [proxy transactionnel] – که مالک تراکنش است – متد [rollback] را روی تراکنش فراخوانی می‌کند تا تمام به‌روزرسانی‌ها را لغو کند، سپس اجازه می‌دهد استثناء تا لایه [web] که مسئول رسیدگی به آن است، منتقل شود.

در مرحله ۴، فرض کردیم که یکی از عملیات درج یا به‌روزرسانی با خطا مواجه شده است. اگر اینطور نباشد، هیچ استثنایی در [5] منتقل نمی‌شود. همین امر در مورد [6] نیز صدق می‌کند. در این حالت، متد [saveMany] از کلاس [proxy transactionnel] یک [commit] روی تراکنش انجام می‌دهد تا تمام به‌روزرسانی‌ها را commit کند.

اکنون تصویر واضح‌تری از معماری پیاده‌سازی‌شده توسط bean [TransactionProxyFactoryBean] داریم. بیایید بار دیگر به پیکربندی آن نگاهی بیندازیم:


    <!-- مدیر تراکنش -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!--کلاس دسترسی برای لایه [service] -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>

بیایید این پیکربندی را با توجه به معماری که ایجاد شده است بررسی کنیم:

  • [proxy transactionnel] مسئول مدیریت تراکنش‌ها خواهد بود. Spring چندین استراتژی مدیریت تراکنش ارائه می‌دهد. [proxy transactionnel] به مرجعی به مدیر تراکنش انتخاب‌شده نیاز دارد.
  • خطوط ۱۱–۱۳: ویژگی [transactionManager] از bean [TransactionProxyFactoryBean] را با ارجاع به مدیر تراکنش تعریف می‌کنند. این در خطوط ۲–۷ تعریف شده است.
  • خطوط ۲–۷: مدیر تراکنش از نوع [DataSourceTransactionManager] است:

Image

[DataSourceTransactionManager] یک مدیر تراکنش است که برای تراکنش‌های SGBD که از طریق یک شیء [DataSource] به آن دسترسی پیدا می‌شود، طراحی شده است. این مدیر تراکنش تنها می‌تواند تراکنش‌ها را روی یک SGBD مدیریت کند. این مدیر نمی‌تواند تراکنش‌های توزیع‌شده در چندین نمونه از SGBD را مدیریت کند. در این مورد، ما فقط یک SGBD داریم. بنابراین، این مدیر تراکنش مناسب است. وقتی [proxy transactionnel] یک تراکنش را آغاز می‌کند، این کار را روی یک اتصال متصل به نخ انجام می‌دهد. همین اتصال است که در تمام لایه‌هایی که به پایگاه داده منتهی می‌شوند استفاده خواهد شد: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].

کلاس [DataSourceTransactionManager] باید منبع داده را بشناسد تا از آن یک اتصال درخواست کند و آن را به نخ (thread) متصل سازد. این موضوع در خطوط ۴ تا ۶ تعریف شده است: این همان منبع داده است که توسط لایه [dao] استفاده می‌شود (به بخش ۱۷.۵.۲ مراجعه کنید).

  • خطوط ۱۴–۱۹: ویژگی «target» کلاسی را که باید رهگیری شود مشخص می‌کند، در این مورد کلاس [ServiceImpl]. این اطلاعات به دو دلیل لازم است:
    • کلاس [ServiceImpl] باید نمونه سازی شود، زیرا مسئول برقراری ارتباط با لایه [dao] است
    • کلاس [TransactionProxyFactoryBean] باید یک پروکسی تولید کند که همان رابط را به لایه [web] ارائه دهد، همانند کلاس [ServiceImpl].
  • خطوط ۲۱–۲۷: مشخص می‌کنند که کدام متدهای [ServiceImpl] باید توسط پروکسی رهگیری شوند. ویژگی [transactionAttributes] در خط ۲۱ مشخص می‌کند که کدام متدهای [ServiceImpl] به یک تراکنش نیاز دارند و ویژگی‌های آن تراکنش چیست:
  • خط ۲۳: متدهایی که نامشان با 'get [getOne, getAll]' شروع می‌شود، در داخل یک تراکنش با ویژگی [PROPAGATION_REQUIRED,readOnly] اجرا می‌شوند:
    • PROPAGATION_REQUIRED: متد در صورتی که یک تراکنش از قبل به نخ متصل باشد، در همان تراکنش اجرا می‌شود؛ در غیر این صورت، یک تراکنش جدید ایجاد شده و متد در آن اجرا می‌شود.
    • readOnly: تراکنش فقط-خواندنی

در اینجا، متدهای [getOne] و [getAll] از [ServiceImpl] در داخل یک تراکنش اجرا خواهند شد، هرچند که این امر در واقع ضروری نیست. در هر مورد، این یک عملیات متشکل از یک دستور SELECT است. ما دلیلی برای قرار دادن این SELECT در یک تراکنش نمی‌بینیم.

  • خط 24: متدهایی که نامشان با «save» شروع می‌شود، مانند [saveOne, saveMany]، در داخل یک تراکنش با ویژگی [PROPAGATION_REQUIRED] اجرا می‌شوند.
  • خط ۲۵: متدهای [deleteOne] و [deleteMany] از [ServiceImpl] به‌طور یکسان با متدهای [saveOne, saveMany] پیکربندی شده‌اند.

در لایه [service] ما، تنها متدهای [saveMany] و [deleteMany] نیاز دارند که در داخل یک تراکنش اجرا شوند. پیکربندی می‌توانست به خطوط زیر کاهش یابد:


        <property name="transactionAttributes">
            <props>
                <prop key="saveMany">PROPAGATION_REQUIRED</prop>
                <prop key="deleteMany">PROPAGATION_REQUIRED</prop>
            </props>
</property>

17.6. آزمایش لایه [service]

اکنون که لایه [service] را نوشته‌ایم و پیکربندی کرده‌ایم، قصد داریم آن را با استفاده از تست‌های JUnit آزمایش کنیم:

Image

فایل پیکربندی [spring-config-test-service-firebird.xml] برای لایه [service] همان است که در بخش 17.5.2 توضیح داده شده است.

آزمون JUnit [TestServiceFirebird] به شرح زیر است:

package istia.st.mvc.personnes.tests;

...

public class TestServiceFirebird extends TestCase {

     // [service] لایه
    private IService service;

    public IService getService() {
        return service;
    }

    public void setService(IService service) {
        this.service = service;
    }

     // راه‌اندازی
    public void setUp() {
        service = (IService) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-service-firebird.xml"))).getBean("service");
    }

     // فهرست افراد
    private void doListe(Collection personnes) {
...
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // اصلاح یا حذف یک آیتم موجود نیست
    public void test2() throws ParseException {
...
    }

     //مدیریت نسخه افراد
    public void test3() throws ParseException, InterruptedException {
...
    }

     //قفل‌گذاری خوش‌بینانه – دسترسی چندرشته‌ای
    public void test4() throws Exception {
...
    }

     //بررسی‌های اعتبار برای saveOne
    public void test5() throws ParseException {
...
    }

         //درج‌های چندرشته‌ای
    public void test6() throws ParseException, InterruptedException{
...
    }

     // آزمایش‌های روش deleteMany
    public void test7() throws ParseException {
         // فهرست فعلی
        Collection personnes = service.getAll();
        int nbPersonnes1 = personnes.size();
         //نمایش
        doListe(personnes);
         // ایجاد سه نفر
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        Personne p2 = new Personne(-1, "Y", "Y", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/03/2006"), false, 0);
        Personne p3 = new Personne(-2, "Z", "Z", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/04/2006"), true, 2);
         // افزودن ۳ نفر – شخص p3 با شناسه -2 باعث خواهد شد 
         // یک استثنا
        boolean erreur = false;
        try {
            service.saveMany(new Personne[] { p1, p2, p3 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
         // تأیید
        assertTrue(erreur);
         // فهرست جدید – تعداد آیتم‌ها نباید تغییر کرده باشد
         // به دلیل بازگشت خودکار تراکنش
        int nbPersonnes2 = service.getAll().size();
        assertEquals(nbPersonnes1, nbPersonnes2);
         // افزودن دو فرد معتبر
         // IDهای آن‌ها را به -1 بازنشانی می‌کنیم
        p1.setId(-1);
        p2.setId(-1);
        service.saveMany(new Personne[] { p1, p2 });
         //بازیابی شناسه‌های آن‌ها
        int id1 = p1.getId();
        int id2 = p2.getId();
         // بررسی‌ها
        p1 = service.getOne(id1);
        assertEquals(p1.getNom(), "X");
        p2 = service.getOne(id2);
        assertEquals(p2.getNom(), "Y");
         // فهرست جدید – باید ۲ عنصر دیگر وجود داشته باشد
        int nbPersonnes3 = service.getAll().size();
        assertEquals(nbPersonnes1 + 2, nbPersonnes3);
         // حذف p1 و p2 و یک شخص وجود ندارد
         //یک استثنا باید رخ دهد
        erreur = false;
        try {
            service.deleteMany(new int[] { id1, id2, -1 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
         //بررسی
        assertTrue(erreur);
         // فهرست جدید
        personnes = service.getAll();
        int nbPersonnes4 = personnes.size();
         // هیچ شخصی نباید حذف می‌شد (rollback
         // معامله)
        assertEquals(nbPersonnes4, nbPersonnes3);
         // هر دو فرد معتبر در حال حذف شدن هستند
        service.deleteMany(new int[] { id1, id2 });
         // بررسی‌ها
         // شخص p1
        erreur = false;
        int codeErreur = 0;
        try {
            p1 = service.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         //باید یک خطای کد ۲ رخ داده باشد
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // شخص p2
        erreur = false;
        codeErreur = 0;
        try {
            p1 = service.getOne(id2);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         //باید یک خطای کد ۲ وجود داشته باشد
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // فهرست جدید
        personnes = service.getAll();
        int nbPersonnes5 = personnes.size();
         // بررسی – ما باید به نقطهٔ شروع بازگشت کرده باشیم
        assertEquals(nbPersonnes5, nbPersonnes1);
         //نمایش
        doListe(personnes);
    }

}
  • خطوط ۱۹–۲۲: برنامه لایه‌های [dao] و [service] را که توسط فایل [spring-config-test-service-firebird.xml] پیکربندی شده‌اند، آزمایش می‌کند، فایلی که در بخش قبلی بررسی شد.
  • تست‌های [test1] تا [test6] از نظر مفهومی با همتایان خود با همین نام در کلاس تست [TestDaoFirebird] از لایه [dao] یکسان هستند. تنها تفاوت این است که، بر اساس پیکربندی، متدهای [saveOne] و [deleteOne] اکنون در داخل یک تراکنش اجرا می‌شوند.
  • هدف متد [test7]، تست متدهای [saveMany] و [deleteMany] است. ما می‌خواهیم تأیید کنیم که آن‌ها واقعاً در داخل یک تراکنش اجرا می‌شوند. بیایید روی کد این متد کامنت بگذاریم:
  • خطوط ۶۲–۶۳: با استفاده از [nbPersonnes1] تعداد افراد حاضر در لیست را می‌شماریم
  • خطوط ۶۷–۷۲: سه نفر ایجاد می‌شوند
  • خطوط ۷۳–۸۳: این سه نفر با استفاده از روش [saveMany] ذخیره می‌شوند – خط ۷۷. دو نفر اول، p1 و p2، که شناسه‌هایشان -1 است، به جدول [PERSONNES] اضافه خواهند شد. شخص p3، از سوی دیگر، شناسه -2 دارد. بنابراین این یک درج نیست بلکه یک به‌روزرسانی است. این به‌روزرسانی ناموفق خواهد بود زیرا هیچ شخصی با شناسه -2 در جدول [PERSONNES] وجود ندارد. لایه [dao] بنابراین یک استثنا (exception) را پرتاب (throw) خواهد کرد که تا لایه [service] بالا می‌رود. وجود این استثنا در خط ۸۳ بررسی می‌شود.
  • به دلیل استثنای قبلی، لایه [service] باید برای تمام سفارش‌های SQL که در حین اجرای متد [saveMany] صادر شده‌اند، یک [rollback] تولید کند، زیرا این متد در داخل یک تراکنش اجرا می‌شود. خطوط 86–87: ما بررسی می‌کنیم که تعداد افراد در لیست تغییر نکرده است و بنابراین، درج‌های p1 و p2 انجام نشده است.
  • خطوط ۸۸–۱۰۳: تنها p1 و p2 اضافه می‌شوند و سپس تأیید می‌شود که دو نفر دیگر در لیست وجود دارند.
  • خطوط ۱۰۶–۱۱۴: ما یک گروه از افراد شامل p1 و p2 که همین حالا اضافه کرده‌ایم و یک شخص وجود ندارد (id = –1) را حذف می‌کنیم. برای این کار از متد [deleteMany] در خط ۱۰۸ استفاده می‌شود. این متد شکست خواهد خورد زیرا هیچ فردی با id برابر با –۱ در جدول [PERSONNES] وجود ندارد. لایه [dao] در نتیجه یک استثنا (exception) را پرتاب (throw) می‌کند که تا لایه [service] propagate (منتقل) می‌شود. وجود این استثنا در خط 114 بررسی می‌شود.
  • به دلیل استثنای قبلی، لایه [service] باید یک [rollback] از تمام سفارش‌های SQL صادر شده در حین اجرای متد [deleteMany]، تولید کند، این به این دلیل است که این متد در داخل یک تراکنش اجرا می‌شود. خطوط ۱۱۶–۱۱۷: ما بررسی می‌کنیم که تعداد افراد در لیست تغییر نکرده است و بنابراین p1 و p2 حذف نشده‌اند.
  • خط ۱۲۲: گروهی که تنها شامل افراد p1 و p2 است حذف می‌شود. این باید با موفقیت انجام شود. بقیه متد تأیید می‌کند که این امر واقعاً رخ داده است.

اجرای تست‌ها نتایج زیر را تولید می‌کند:

Image

تمام هفت تست با موفقیت انجام شد. ما لایه خود [service] را عملیاتی در نظر می‌گیریم.

17.7. لایه [web]

بیایید معماری کلی وب‌اپلیکیشن مورد نظر را که باید ساخته شود، به یاد آوریم:

ما به تازگی لایه‌های [dao] و [service] را ساخته‌ایم که به ما امکان کار با پایگاه داده Firebird را می‌دهند. ما نسخهٔ ۱ این برنامه را نوشتیم که در آن لایه‌های [dao] و [service] با فهرستی از افراد ذخیره شده در حافظه کار می‌کردند. لایه [web] که در آن زمان نوشته شده بود، همچنان معتبر است. این به این دلیل است که این لایه برای لایه [service] طراحی شده بود که رابط [IService] را پیاده‌سازی می‌کرد. از آنجا که لایه جدید [service] همین رابط را پیاده‌سازی می‌کند، نیازی به تغییر لایه [web] نیست.

در مقاله قبلی، نسخه ۱ برنامه با پروژه Eclipse [mvc-personnes-02B] آزمایش شده بود، که در آن لایه‌های [web, service, dao, entites] در آرشیوهای .jar قرار داده شده بودند:

پوشه [src] خالی بود. کلاس‌های لایه در آرشیوهای [personnes-*.jar ] قرار داشتند:

برای آزمایش نسخهٔ ۲، در اکلیپس پوشهٔ Eclipse با نام [mvc-personnes-02B] را با نام [mvc-personnes-03B] (با کپی و پیست) کپی می‌کنیم:

Image

در پروژه [mvc-personnes-03]، ما لایه‌های [dao] و [service] را از [File / Export / Jar file] به ترتیب به آرشیوهای [personnes-dao.jar] و [personnes-service.jar] صادر می‌کنیم، در داخل [dist] پروژه:

Image

این دو فایل را کپی می‌کنیم، سپس در اکلیپس آن‌ها را در پوشه [WEB-INF/lib] پروژه [mvc-personnes-03B] می‌چسبانیم، جایی که جایگزین آرشیوهای هم‌نام از نسخه قبلی خواهند شد.

ما همچنین آرشیوهای [commons-dbcp-*.jar, commons-pool-*.jar, firebirdsql-full.jar, ibatis-common-2.jar, ibatis-sqlmap-2.jar] را از پوشه [lib] پروژه [mvc-personnes-03] کپی و در پوشه [WEB-INF/lib] پروژه [mvc-personnes-03B] الصاق می‌کنیم. این آرشیوها برای لایه‌های جدید [dao] و [service] لازم هستند.

پس از انجام این کار، آرشیوهای جدید را در مسیر کلاس پروژه (classpath) وارد می‌کنیم: [clic droit sur projet -> Properties -> Java Build Path -> Add Jars].

پوشه [src] حاوی فایل‌های پیکربندی برای لایه‌های [dao] و [service] است:

Image

فایل [spring-config.xml] لایه‌های [dao] و [service] از برنامه وب را پیکربندی می‌کند. در نسخه جدید، این فایل با فایل [spring-config-test-service-firebird.xml] یکسان است که برای پیکربندی تست لایه سرویس در پروژه [mvc-personnes-03] استفاده می‌شد. بنابراین ما از یکی به دیگری کپی و پیست می‌کنیم:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- منبع داده DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <property name="url">
            <!-- لطفاً توجه داشته باشید: بین دو تگ <value> هیچ فاصله‌ای قرار ندهید -->
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!--کلاس دسترسی برای لایه [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!--مدیر تراکنش -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- کلاس‌های دسترسی برای لایه [service] -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
</beans>
  • خط ۱۲: URL پایگاه داده Firebird. ما به استفاده از پایگاه داده مورد استفاده برای تست لایه‌های [dao] و [service] ادامه می‌دهیم.

ما پروژه وب [mvc-personnes-03B] را در Tomcat مستقر می‌کنیم:

ما برای تست آماده هستیم. نمونه Firebird با شناسه SGBD راه‌اندازی شده است. سپس محتویات جدول [PERSONNES] به شرح زیر است:

Image

سپس Tomcat راه‌اندازی می‌شود. با استفاده از یک مرورگر وب، URL [http://localhost:8080/mvc-personnes-03B] را درخواست می‌کنیم:

Image

ما اضافه می‌کنیم یک شخص جدید از طریق لینک [Ajout] اضافه می‌کنیم:

ما افزودن را در پایگاه داده بررسی می‌کنیم:

Image

از خواننده دعوت می‌شود تا آزمایش‌های بیشتری را انجام دهد: [modification, suppression].

اکنون بیایید آزمون تداخل نسخه را که در نسخهٔ ۱ انجام شد، اجرا کنیم. [Firefox] مرورگر کاربر خواهد بود U1. این مرورگر URL [http://localhost:8080/mvc-personnes-03B] را درخواست می‌کند:

Image

[IE] مرورگر کاربر U2 خواهد بود. این کاربر همان URL را درخواست می‌کند:

Image

کاربر U1 وارد صفحه ویرایش شخص [Perrichon] می‌شود:

Image

کاربر U2 نیز همین کار را انجام می‌دهد:

Image

کاربر U1 تغییرات را اعمال و ذخیره می‌کند:

کاربر U2 همین کار را انجام می‌دهد:

کاربر U2 از طریق لینک [Annuler] در فرم به فهرست افراد بازمی‌گردد:

Image

آن‌ها شخص [Perrichon] را که توسط U1 (نام بزرگ‌نویس شده) اصلاح شده است، پیدا می‌کنند.

و در همه این‌ها، پایگاه داده چه وضعیتی دارد؟ بیایید نگاهی بیندازیم:

Image

نام شخص شمارهٔ ۸۹۹ در واقع پس از اصلاح انجام‌شده توسط U1 به حروف بزرگ نوشته شده است.

17.8. Conclusion

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

که لایه‌های [dao] و [service] با فهرستی از داده‌ها در حافظه کار می‌کردند، بنابراین هنگام خاموش شدن وب‌سرور از بین می‌رفتند. این نسخه ۱ بود. در نسخه ۲، لایه‌های [service] و [dao] بازنویسی شدند تا لیست افراد در یک جدول پایگاه داده ذخیره شود. بنابراین اکنون پایدار است. اکنون تأثیر تغییری که در SGBD ایجاد شده است را بر برنامه خود بررسی خواهیم کرد. برای این کار، سه نسخه جدید از برنامه وب خود را خواهیم ساخت:

  • نسخهٔ ۳: SGBD از Postgres استفاده می‌کند
  • نسخه ۴: SGBD است MySQL
  • نسخهٔ ۵: SGBD برابر است با SQL Server Express 2005

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

  • کلاس [DaoImplFirebird] قابلیت‌هایی را از لایه [dao] که مربوط به SGBD Firebird است، پیاده‌سازی می‌کند. اگر این نیاز همچنان پابرجا باشد، به ترتیب با کلاس‌های [DaoImplPostgres]، [DaoImplMySQL] و [DaoImplSqlExpress] جایگزین خواهد شد.
  • فایل نگاشت [personnes-firebird.xml] از iBATIS برای SGBD Firebird با فایل‌های نگاشت [personnes-postgres.xml] جایگزین خواهد شد، [personnes-mysql.xml] و [personnes-sqlexpress.xml] به ترتیب.
  • پیکربندی شیء [DataSource] در لایه [dao] مختص SGBD است. بنابراین با هر نسخه تغییر خواهد کرد.
  • راننده JDBC برای SGBD نیز با هر نسخه تغییر می‌کند

به جز این موارد، سایر موارد بدون تغییر باقی می‌مانند. در ادامه، این نسخه‌های جدید را با تمرکز صرف بر ویژگی‌های جدیدی که هر یک معرفی می‌کنند، شرح می‌دهیم.