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

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

علاوه بر جدول [PERSONNES]، پایگاه داده [dbpersonnes.gdb] شامل ابجکتی به نام ژنراتور به نام [GEN_PERSONNES_ID] است. این ژنراتور اعداد صحیح متوالی تولید میکند که از آنها برای تخصیص مقدار به کلید اصلی [ID] از کلاس [PERSONNES] استفاده خواهیم کرد. بیایید برای روشن شدن نحوه کار یک مثال بزنیم:
![]() |
![]() |
میتوانیم ببینیم که مقدار ژنراتور [GEN_PERSONNES_ID] تغییر کرده است (برای تازهسازی روی آن دوبار کلیک کنید + F5):
sequence SQL
بنابراین این مقدار زیر برای ژنراتور حاصل میشود: [GEN_PERSONNES_ID]. GEN_ID یک تابع داخلی Firebird است و [RDB$DATABASE] یک جدول سیستمی برای SGBD است.
17.2. پروژه اکلیپس برای لایههای [dao] و [service]
برای توسعه لایههای [dao] و [service] از برنامه کاربردی پایگاه داده خود، از پروژه اکلیپس زیر، [mvc-personnes-03]، استفاده خواهیم کرد:

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

این پوشه شامل بستههای مختلفی است:
- [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 است:
![]()
- [dbpersonnes.gdb] پایگاه داده است.
- [dbpersonnes.sql] اسکریپت SQL برای تولید پایگاه داده است:
پوشه [lib]
این پوشه شامل آرشیوهای مورد نیاز برنامه است:
![]() |
شایان ذکر است که درایور JDBC [firebirdsql-full.jar] برای SGBD Firebird، و همچنین تعدادی آرشیو [spring-*.jar] موجود است. میتوانستیم از آرشیو واحد [spring.jar] موجود در پوشه [dist] توزیع استفاده کنیم که شامل تمام کلاسهای Spring است. همچنین میتوان تنها از آرشیوهای لازم برای پروژه استفاده کرد. این کاری است که ما در اینجا انجام دادهایم، با راهنمایی خطاهای «کلاس مفقود» گزارششده توسط Eclipse و نامهای آرشیوهای جزئی Spring. تمام این آرشیوها از پوشه [lib] در پوشه Classpath پروژه قرار داده شدهاند.
پوشه [dist]
این پوشه حاوی آرشیوهای حاصل از کامپایل شدن کلاسهای برنامه خواهد بود:
![]()
- [personnes-dao.jar]: آرشیو لایه [dao]
- [personnes-service.jar]: آرشیو لایه [service]
17.3. لایه [dao]
17.3.1. اجزای لایه [dao]
لایه [dao] شامل کلاسها و رابطهای زیر است:

- [IDao] رابطی است که توسط لایه [dao] ارائه میشود
- [DaoImplCommon] پیادهسازی این رابط است که در آن گروه افراد در یک جدول پایگاه داده ذخیره میشود. [DaoImplCommon] عملکردهایی را که از SGBD مستقل هستند، در کنار هم قرار میدهد.
- [DaoImplFirebird] کلاسی مشتقشده از [DaoImplCommon] است که بهطور خاص برای مدیریت پایگاه داده Firebird طراحی شده است.
- [DaoException] نوع استثناهای رسیدگینشدهای است که توسط لایه [dao] پرتاب میشوند. این کلاس با نسخه ۱ مطابقت دارد.
رابط [IDao] به شرح زیر است:
- این رابط همان چهار متد نسخه قبلی را دارد.
کلاس [DaoImplCommon] که این رابط را پیادهسازی میکند، به شرح زیر خواهد بود:
- خطوط ۸–۹: کلاس [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/] در دسترس است، استفاده میکند:

[iBATIS] یک پروژه آپاچی است که ساخت لایههای [dao] مبتنی بر پایگاهدادهها را تسهیل میکند. با [iBATIS]، معماری لایه دسترسی به داده به شرح زیر است:
![]() |
[iBATIS] بین لایه [dao] برنامه و درایور JDBC پایگاه داده قرار دارد. برای [iBATIS] جایگزینهایی وجود دارد، مانند، برای مثال، جایگزین [Hibernate]:

![]() |
استفاده از چارچوب [iBATIS] نیازمند دو آرشیو [ibatis-common, ibatis-sqlmap] است که هر دو در پوشه [lib] پروژه قرار داده شدهاند:
![]() |
کلاس [SqlMapClientDaoSupport] بخش عمومی فریمورکهای [iBATIS] و c.a.d را در بر میگیرد. بخشهای کدی که در تمام لایههای [dao] با استفاده از ابزار [iBATIS] یافت میشوند. برای نوشتن بخش غیرکلی کد – یعنی بخشی که مختص لایه [dao] است – کافی است از کلاس [SqlMapClientDaoSupport] ارثبری کنید. این کاری است که ما در اینجا انجام میدهیم.
کلاس [SqlMapClientDaoSupport] به صورت زیر تعریف شده است:

در میان متدهای این کلاس، یکی از آنها به شما امکان میدهد کلاینت [iBATIS] را که با آن به پایگاه داده دسترسی خواهید داشت، پیکربندی کنید:
![]()
شیء [SqlMapClient sqlMapClient] همان شیء [IBATIS] است که برای دسترسی به پایگاه داده استفاده میشود. به خودی خود، این شیء لایه [iBATIS] معماری ما را پیادهسازی میکند:
![]() |
یک توالی معمول از عملیات شامل این شیء به شرح زیر است:
- درخواست اتصال از استخر اتصالات
- باز کردن یک تراکنش
- اجرای مجموعهای از دستورات SQL که در یک فایل پیکربندی ذخیره شدهاند
- بستن تراکنش
- بازگرداندن اتصال به استخر
اگر پیادهسازی ما از [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] است:

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

به یاد داشته باشید که ما در پی ایجاد یک شیء هستیم که رابط [SqlMapClient] را پیادهسازی میکند. به نظر نمیرسد این مورد برای کلاس [SqlMapClientFactoryBean] صدق کند. این کلاس رابط [FactoryBean] را پیادهسازی میکند (به بالا مراجعه کنید). این کلاس متد زیر را دارد، [getObject()]:
![]()
وقتی از Spring خواسته میشود نمونهای از یک شیء پیادهکننده رابط [FactoryBean] را فراهم کند، آن:
- یک نمونه از کلاس [I] ایجاد میکند – در این مورد، یک نمونه از نوع [SqlMapClientFactoryBean] ایجاد میکند.
- نتیجهٔ متد `[I].getObject()` را به متد فراخوانیکننده بازمیگرداند – متد `[SqlMapClientFactoryBean].getObject() یک شیء پیادهسازیکنندهٔ رابط [SqlMapClient] را بازمیگرداند.
برای بازگرداندن یک شیء پیادهسازیکنندهٔ رابط [SqlMapClient]، کلاس [SqlMapClientFactoryBean] به دو مورد اطلاعات لازم برای این شیء نیاز دارد:
- یک شیء [DataSource] متصل به پایگاه داده، که از آن درخواست اتصال خواهد کرد
- یک (یا چند) فایل پیکربندی حاوی دستورات SQL برای اجرا
کلاس [SqlMapClientFactoryBean] متدهایی را برای مقداردهی اولیه این دو ویژگی فراهم کرده است:

ما در حال پیشرفت هستیم… فایل پیکربندی ما در حال شکلگیری است و اکنون اینگونه خوانده میشود:
<!-- 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] است:

بار دیگر، با یک رابط سر و کار داریم که باید برای آن یک کلاس پیادهسازی پیدا کنیم. نقش چنین کلاسی این است که به طور کارآمد اتصالات به یک پایگاه داده خاص را برای یک برنامه فراهم کند. یک SGBD نمیتواند تعداد زیادی اتصال را بهطور همزمان باز نگه دارد. برای کاهش تعداد اتصالات باز در هر لحظه، برای هر تعامل با پایگاه داده، باید:
- باز کردن یک اتصال
- شروع یک تراکنش
- صدور دستورات SQL
- تعامل را ببندیم
- اتصال را بستن
باز و بسته کردن مکرر اتصالات، وقتگیر است. برای حل این دو مشکل (هم محدود کردن تعداد اتصالات باز در هر لحظه و هم به حداقل رساندن هزینه باز و بسته کردن آنها)، کلاسهایی که رابط [DataSource] را پیادهسازی میکنند، اغلب به شرح زیر عمل میکنند:
- در هنگام نمونهسازی، آنها N اتصال به پایگاه داده مقصد را باز میکنند. N معمولاً یک مقدار پیشفرض دارد و معمولاً میتوان آن را در یک فایل پیکربندی تعریف کرد. این N اتصال همیشه باز میمانند و یک استخر اتصال را برای نخهای برنامه فراهم میکنند.
- هنگامی که یک نخ برنامه (application thread) درخواست اتصال میدهد، شیء [DataSource] در صورت باقی ماندن اتصالهای در دسترس، یکی از N اتصال باز شده در زمان راهاندازی را در اختیار آن قرار میدهد. هنگامی که برنامه اتصال را میبندد، آن اتصال در واقع بسته نمیشود، بلکه صرفاً به استخر اتصالات در دسترس بازگردانده میشود.
پیادهسازیهای مختلفی از رابط [DataSource] به صورت رایگان در دسترس هستند. در اینجا، ما از پیادهسازی [commons DBCP] که در آدرس URL [http://jakarta.apache.org/commons/dbcp/] در دسترس است، استفاده خواهیم کرد:

استفاده از ابزار [commons DBCP] به دو آرشیو [commons-dbcp, commons-pool] نیاز دارد که هر دو در پوشه [lib] پروژه قرار داده شدهاند:
![]() |
کلاس [BasicDataSource] از [commons DBCP] پیادهسازی [DataSource] مورد نیاز ما را فراهم میکند:

این کلاس یک استخر اتصال برای دسترسی به پایگاه داده Firebird برنامه ما [dbpersonnes.gdb] را فراهم میکند. برای این کار، باید اطلاعات لازم برای ایجاد اتصالات در استخر را در اختیار آن قرار دهیم:
- نام درایور مورد استفاده – مقداردهی اولیه شده با
- آدرس URL پایگاه داده مورد استفاده – مقدار اولیه [setUrl]
- نام کاربری کاربری که مالک اتصال است – مقدار اولیه [setUsername] (و نه setUserName همانطور که ممکن بود انتظار رود)
- رمز عبور آنها – مقدار اولیه [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 ما کجا قرار دارند:

- [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] به شرح زیر است:
ما متدها را یکی یکی بررسی خواهیم کرد.
getAll
این متد تمام افراد موجود در لیست را بازیابی میکند. کد آن به شرح زیر است:
ابتدا بیایید به یاد داشته باشیم که کلاس [DaoImplCommon] از کلاس Spring یعنی [SqlMapClientDaoSupport] ارث میبرد. این کلاس حاوی متد [getSqlMapClientTemplate()] است که در خط ۳ بالا استفاده شده است. این متد امضای زیر را دارد:
![]()
نوع [SqlMapClientTemplate] شیء [SqlMapClient] را از لایه [iBATIS] در بر میگیرد. این از طریق همین نوع است که ما به پایگاه داده دسترسی پیدا خواهیم کرد. نوع [iBATIS] SqlMapClient میتواند مستقیماً استفاده شود، زیرا کلاس [SqlMapClientDaoSupport] به آن دسترسی دارد:
![]()
نقطه ضعف کلاس [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] به شرح زیر تعریف شده است:

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

این رابط متدهایی را تعریف میکند که قادر به استفاده از محتویات فایل [personnes-firebird.xml] هستند:
[queryForList]
![]()
این متد به شما امکان میدهد تا یک فرمان [SELECT] را صادر کرده و نتیجه را به صورت یک لیست از اشیاء بازیابی کنید:
- [statementName]: شناسهی (id) دستور [select] در فایل پیکربندی
- [parameterObject]: شیء «پارامتر» برای یک [select] پیکربندیشده. شیء «پارامتر» میتواند دو شکل داشته باشد:
- یک شیء منطبق با استاندارد JavaBean: پارامترهای دستور [select] در این صورت نام فیلدهای JavaBean خواهند بود. هنگامی که دستور [select] اجرا میشود، این نامها با مقادیر این فیلدها جایگزین میشوند.
- یک دیکشنری: پارامترهای فرمان [select] در این صورت کلیدهای دیکشنری هستند. وقتی فرمان [select] اجرا میشود، اینها با مقادیر متناظرشان در دیکشنری جایگزین میشوند.
- اگر [SELECT] هیچ ردیفی بازنمیگرداند، نتیجه [List] یک شیء خالی است، اما null نیست (برای تأیید).
[queryForObject]
![]()
این روش از نظر اصول مشابه روش قبلی است، اما تنها یک شیء را برمیگرداند. اگر [SELECT] هیچ ردیفی را برنگرداند، نتیجه، نشانگر null است.
[insert]
![]()
این متد به شما امکان میدهد تا یک دستور SQL [insert] را که توسط پارامتر دوم پیکربندی شده است، اجرا کنید. شیء بازگشتی، کلید اصلی ردیفی است که درج شده است. استفاده از این نتیجه الزامی نیست.
[update]
![]()
این متد یک دستور SQL [update] را که توسط پارامتر دوم پیکربندی شده است، اجرا میکند. نتیجه، تعداد ردیفهایی است که توسط دستور SQL [update] تغییر یافتهاند.
[delete]
![]()
این متد فرمان SQL [delete] را که توسط پارامتر دوم پیکربندی شده است اجرا میکند. نتیجه، تعداد سطرهایی است که توسط فرمان SQL [delete] حذف شدهاند.
بیایید به متد [getAll] کلاس [DaoImplCommon] بازگردیم:
- خط ۴: دستور [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] شناسایی شده است بازیابی کنید. کد به شرح زیر است:
- خط ۴: اجرای فرمان [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] شناسایی شده است. کد آن به شرح زیر است:
- خطوط ۴–۵: اجرای دستور [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
این روش به شما امکان میدهد یک شخص جدید اضافه کنید یا شخص موجود را ویرایش کنید. کد آن به شرح زیر است:
- خط ۴: اعتبار شخص با استفاده از روش [check] بررسی میشود. این روش در نسخه قبلی نیز وجود داشت و در آن زمان غیرفعال شده بود. اگر شخص معتبر نباشد، یک [DaoException] را فعال میکند. ما اجازه میدهیم این خطا به مراحل بعدی ارسال شود.
- خط ۶: اگر به این نقطه برسیم، یعنی هیچ استثنایی رخ نداده است. بنابراین شخص معتبر است.
- خطوط ۶–۱۱: بسته به شناسه شخص، این یا یک افزودن (ID = -1) است یا یک بهروزرسانی (ID ≠ -1). در هر دو مورد، دو متد داخلی کلاس فراخوانی میشوند:
- insertPersonne: برای افزودن
- updatePersonne: برای بهروزرسانی
insertPersonne
این متد برای افزودن یک شخص جدید استفاده میشود. کد آن به شرح زیر است:
- خط ۴: شماره نسخه شخص در حال ایجاد را روی ۱ تنظیم کنید
- خط ۹: رکورد را با استفاده از پرسوجوی با نام "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] در خط ۲ است که این فیلد را مشخص میکند.
- پرسوجوی SQL که باید برای بهدستآوردن یک مقدار کلید اصلی اجرا شود. پرسوجوی نشاندادهشده در اینجا همان پرسوجویی است که در بخش 17.1 ارائه کردیم. دو نکته قابل توجه است:
- خطوط ۶–۷: برای اهداف آزمایشی، باید قبل از درج، ۱۰ میلیثانیه صبر کنیم تا هرگونه تداخل بین نخهایی را که ممکن است همزمان در حال افزودن باشند، بررسی کنیم.
updatePersonne
این روش به شما امکان میدهد فردی را که از قبل در جدول [PERSONNES] وجود دارد، اصلاح کنید. کد آن به شرح زیر است:
- یک بهروزرسانی ممکن است به حداقل دو دلیل ناموفق باشد:
- فردی که قرار است بهروزرسانی شود وجود ندارد
- شخص مورد نظر برای بهروزرسانی وجود دارد، اما رشتهای که قصد اصلاح آن را دارد، نسخه صحیح را ندارد
- خطوط ۷–۸: پرسوجوی 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 آزمایش کنیم:

قبل از انجام آزمایشهای فشرده، میتوانیم با یک برنامه ساده از نوع [main] شروع کنیم که محتویات جدول [PERSONNES] را نمایش میدهد. این کلاس [MainTestDaoFirebird] است:
فایل پیکربندی [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] به شرح زیر است:

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

ما با موفقیت فهرست افراد را بهدست آوردیم. اکنون میتوانیم به تست JUnit بپردازیم.
آزمون JUnit [TestDaoFirebird] به شرح زیر است:
- آزمایشهای [test1] تا [test5] همانند نسخه ۱ هستند، به جز [test4] که کمی تغییر کرده است. آزمایش [test6] جدید است. ما فقط در مورد این دو آزمایش توضیح خواهیم داد.
[test4]
[test4] برای آزمایش روش [updatePersonne - DaoImplCommon] طراحی شده است. کد آن روش به این صورت است:
- خطوط ۴–۵: ما به مدت ۱۰ میلیثانیه منتظر میمانیم. این کار باعث میشود تاثری که [updatePersonne] را اجرا میکند، کنترل پردازنده مرکزی (CPU) را از دست بدهد، که ممکن است شانس ما را برای مشاهده تضادهای دسترسی بین نخهای همزمان افزایش دهد.
[test4] N=100 نخ را راهاندازی میکند که وظیفه دارند همزمان تعداد فرزندان یک شخص را به اندازه 1 افزایش دهند. ما میخواهیم ببینیم چگونه تعارضات نسخهای و تعارضات دسترسی مدیریت میشوند.
رشتهها در خطوط ۸–۱۳ ایجاد شدهاند. هر یک تعداد فرزندان شخص ایجادشده در خطوط ۳–۵ را به اندازه ۱ افزایش میدهد. رشتههای بهروزرسانی [ThreadDaoMajEnfants ] به شرح زیر هستند:
بهروزرسانی یک شخص ممکن است بهدلیل عدم وجود شخص مورد اصلاح یا بهروزرسانی شدن قبلی آن توسط نخ دیگری با شکست مواجه شود. هر دو حالت در اینجا در خطوط 67–69 رسیدگی شدهاند. در هر دو مورد، متد [updatePersonne] یک [DaoException] با کد 2 را راهاندازی میکند. سپس این نخ به شروع مجدد فرآیند بهروزرسانی از ابتدا بازگردانده میشود (حلقه while، خط 34).
[test6]
[test6] برای تست متد [insertPersonne - DaoImplCommon] طراحی شده است. کد آن متد به این صورت است:
- خطوط ۶–۷: ما ۱۰ میلیثانیه صبر میکنیم تا نخ در حال اجرای [insertPersonne]، CPU را از دست بدهد و بدین ترتیب شانس مشاهده تضادهای ناشی از اجرای همزمان عملیات درج توسط نخها را افزایش دهیم.
کد برای [test6] به شرح زیر است:
ما ۱۰۰ نخ ایجاد میکنیم که بهطور همزمان ۱۰۰ شخص مختلف را درج خواهند کرد. این ۱۰۰ نخ همگی یک کلید اصلی برای شخصی که قرار است درج کنند دریافت میکنند، سپس قبل از اینکه بتوانند درج خود را انجام دهند، به مدت ۱۰ میلیثانیه معلق میشوند (خط ۱۰ – insertPersonne). ما میخواهیم بررسی کنیم که همه چیز به درستی کار میکند و به خصوص اینکه آنها واقعاً مقادیر کلید اصلی متفاوتی دریافت میکنند.
- خطوط ۷–۱۱: یک آرایه از ۱۰۰ نفر ایجاد میشود. این افراد همگی کپیهایی از شخص «p» هستند که در خطوط ۴–۵ ایجاد شده است.
- خطوط 14–17: 100 نخ درج راهاندازی میشوند. هر یک مأمور درج یکی از 100 نفری هستند که قبلاً ایجاد شدهاند.
- خطوط ۱۹–۲۳: [test6] منتظر پایان کار هر یک از ۱۰۰ رشتهای که راهاندازی کرده است، میماند. وقتی تشخیص میدهد که رشته با شماره i کارش تمام شده است، فردی را که این رشته به تازگی وارد کرده است، حذف میکند.
رشته درج [ThreadDaoInsertPersonne] به شرح زیر است:
- خطوط ۱۹–۲۲: سازندهٔ نخ، شخص مورد نیاز برای درج و لایهٔ [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] باید اصلاح شود:
- خط ۸: یک استثنا از نوع [DaoException] مدیریت میشود. بر اساس آنچه گفته شد، ما باید استثنایی را که در حین آزمایش رخ داده است، یعنی نوع [org.springframework.jdbc.UncategorizedSQLException]، مدیریت کنیم. با این حال، ما نمیتوانیم به سادگی این نوع را مدیریت کنیم، زیرا این یک نوع عمومی Spring است که برای دربرگیری استثناهایی طراحی شده که آن را تشخیص نمیدهد. اسپرینگ استثناهای پرتابشده توسط درایورهای JDBC را برای تعدادی از سیستمهای SGBD مانند Oracle، MySQL، تشخیص میدهد، Postgres، DB2، SQL Server، … اما نه Firebird. بنابراین، هر استثنایی که توسط درایور Firebird JDBC پرتاب میشود، در نوع Spring [org.springframework.jdbc.UncategorizedSQLException] پیچیده میشود:

همانطور که در بالا مشاهده میشود، کلاس [UncategorizedSQLException] از کلاس [DataAccessException] که در بخش 17.3.3 به آن پرداختیم، ارث میبرد. امکان شناسایی استثنایی که در [UncategorizedSQLException] محصور شده است، از طریق متد [getSQLException] آن وجود دارد:
![]()
این استثنا از نوع [SQLException] همان است که توسط لایه [iBATIS] پرتاب میشود، که خود استثنای پرتابشده توسط درایور پایگاه داده JDBC را در بر میگیرد. علت دقیق استثنای [SQLException] را میتوان با استفاده از روش زیر بهدست آورد:
![]()
این شیء از نوع [Throwable] را بازمیگرداند که توسط درایور JDBC پرتاب شده است:

نوع [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] را میتوان به صورت زیر بازنویسی کرد:
- خطوط ۳۴–۳۶: استثنای [DaoException] با کد ۴ رهگیری میشود. نخ [ThreadDaoMajEnfants] مجبور به راهاندازی مجدد فرآیند بهروزرسانی از ابتدا (خط ۱۰) خواهد شد.
لایه [dao] ما باید قادر باشد استثناء از نوع «تضاد بهروزرسانی» را تشخیص دهد. این استثناء توسط یک درایور JDBC ایجاد میشود و مختص آن است. این استثنا باید در متد [updatePersonne] از کلاس [DaoImplCommon] مدیریت شود:
خطوط ۷ تا ۱۱ باید در داخل یک بلوک try/catch قرار گیرند. برای درایور SGBD Firebird، باید بررسی کنیم که استثنایی که باعث شکست بهروزرسانی شده، از نوع [org.firebirdsql.gds.GDSException] بوده و کد خطای 335544336 را داشته باشد. اگر این نوع تست را در [DaoImplCommon] قرار دهیم، این کلاس را به کلاس SGBD Firebird لینک خواهیم کرد که واضحاً نامطلوب است. اگر بخواهیم کلاس [DaoImplCommon] را با هدف عمومی حفظ کنیم، باید از آن ارث ببریم و مدیریت استثنا را در یک کلاس اختصاصی Firebird انجام دهیم. کاری که اکنون در حال انجام آن هستیم همین است.
17.4.2. کلاس [DaoImplFirebird]
کد آن به شرح زیر است:
- خط ۵: کلاس [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] که قبلاً ناموفق بود، به شرح زیر است:

[test4] با موفقیت انجام شد. خطوط پایانی گزارشهای صفحه به شرح زیر است:
خط آخر نشان میدهد که رشته شماره ۳۶ آخرین رشتهای بود که به پایان رسید. خط ۳ یک تداخل نسخهای را نشان میدهد که رشته شماره ۳۶ را مجبور به راهاندازی مجدد رویه بهروزرسانی افراد کرد (خط ۴). سایر لاگها تداخلهای دسترسی را در حین بهروزرسانیها نشان میدهند:
خط ۲ نشان میدهد که نخ شماره ۷۵ در حین بهروزرسانی خود به دلیل تداخل بهروزرسانی با شکست مواجه شد: هنگامی که دستور SQL [update] بر روی جدول [PERSONNES] صادر شد، ردیفی که نیاز به بهروزرسانی داشت توسط یک نخ دیگر قفل شده بود. این تداخل دسترسی، نخ شماره ۷۵ را مجبور به تلاش مجدد برای بهروزرسانی خود میکند.
در نهایت، در مورد [test4]، تفاوت قابل توجهی با نتایج همان تست در نسخه ۱ وجود دارد که در آن به دلیل مشکلات همگامسازی ناموفق بود. از آنجایی که متدها در لایه [dao] نسخه ۱ همگامسازی نشده بودند، تعارضات دسترسی به وجود آمد. در اینجا، ما نیازی به همگامسازی لایه [dao] نداشتیم. ما به سادگی با تعارضات دسترسی که توسط Firebird گزارش میشد، برخورد کردیم.
اکنون بیایید کل تست JUnit را برای لایه [dao] اجرا کنیم:

بنابراین به نظر میرسد که ما یک لایه معتبر [dao] داریم. برای اعلام اعتبار آن با اطمینان بالا، نیاز به انجام آزمایشهای بیشتری داریم. با این حال، ما آن را عملیاتی در نظر خواهیم گرفت.
17.5. لایه [service]
17.5.1. اجزای لایه [service]
لایه [service] شامل کلاسها و رابطهای زیر است:
![]()
- [IService] رابطی است که توسط لایه [service] ارائه میشود
- [ServiceImpl] پیادهسازی این است
رابط [IService] به شرح زیر است:
- این رابط همان چهار متد نسخهٔ ۱ را دارد، اما دو متد اضافی نیز دارد:
- saveMany: امکان ذخیرهٔ چند نفر را بهطور اتمیک همزمان فراهم میکند. یا همگی ذخیره میشوند یا هیچکدام ذخیره نمیشوند.
- deleteMany: به شما امکان میدهد چندین نفر را به طور همزمان به صورت اتمی حذف کنید. یا همگی حذف میشوند، یا هیچکدام حذف نمیشوند.
این دو متد توسط وباپلیکیشن استفاده نخواهند شد. ما آنها را برای تشریح مفهوم تراکنش پایگاه داده اضافه کردهایم. در واقع، هر دو متد باید در داخل یک تراکنش اجرا شوند تا به یکپارچگی اتمی مورد نظر دست یابند.
کلاس [ServiceImpl] که این رابط را پیادهسازی میکند، به شکل زیر خواهد بود:
- متدهای [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] بیندازیم:

میتوانیم ببینیم که این کلاس، رابط [FactoryBean] را پیادهسازی میکند. ما قبلاً با این رابط مواجه شدهایم. میدانیم که وقتی یک برنامه از Spring یک نمونه از نوعی را که [FactoryBean] را پیادهسازی میکند درخواست میکند، اسپرینگ نه یک نمونه [I] از آن نوع را بازمیگرداند، بلکه شیء بازگرداندهشده توسط متد [I].getObject() را بازمیگرداند.
![]()
در مورد ما، لایه [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 به شرح زیر انجام خواهد شد (به نمودار بالا مراجعه کنید):
- [C1] متد M از [C2] را فراخوانی میکند. در واقع، متد M از [intercepteur] فراخوانی خواهد شد. این امر زمانی امکانپذیر است که [C1] به یک رابط [I2] اشاره کند، نه به یک پیادهسازی خاص از [I2]. پس، تنها کاری که لازم است انجام شود این است که [intercepteur]، [I2] را پیادهسازی کند.
- روش M در [intercepteur] خروجی را ثبت میکند و روش M در [C2] را فراخوانی میکند که در ابتدا هدف [C1] بود.
- روش M در [C2] اجرا میشود و نتیجهٔ خود را به روش M در [intercepteur] بازمیگرداند، که میتواند اختیاریاً چیزی را به آنچه در مرحلهٔ ۲ انجام شده اضافه کند.
- روش 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] را با مفهوم تراکنش گسترش خواهد داد. بیایید نمودار بالا را دنبال کنیم:
- لایه [web] متد [saveMany] از رابط [IService] را فراخوانی میکند.
- متد [saveMany] از کلاس [proxy transactionnel] اجرا میشود. این متد یک تراکنش را آغاز میکند. این لایه باید اطلاعات کافی برای انجام این کار را داشته باشد، بهویژه یک شیء [DataSource] برای برقراری ارتباط با SGBD. سپس متد [saveMany] از [ServiceImpl] را فراخوانی میکند.
- این متد اجرا میشود. این متد به طور مکرر لایه [dao] را برای انجام درجها یا بهروزرسانیها فراخوانی میکند. دستورات SQL که در این مرحله اجرا میشوند، در چارچوب تراکنش آغازشده در مرحله ۲ انجام میگیرند.
- فرض کنید یکی از این عملیات با شکست مواجه شود. لایه [dao] اجازه میدهد استثناء تا لایه [service]، در این مورد متد [saveMany] از نمونه [ServiceImpl]، منتقل شود.
- این متد هیچ کاری انجام نمیدهد و اجازه میدهد استثناء تا متد [saveMany] از [proxy transactionnel] propagate شود.
- پس از دریافت استثنا، متد [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] است:

[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 آزمایش کنیم:

فایل پیکربندی [spring-config-test-service-firebird.xml] برای لایه [service] همان است که در بخش 17.5.2 توضیح داده شده است.
آزمون JUnit [TestServiceFirebird] به شرح زیر است:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
- خطوط ۱۹–۲۲: برنامه لایههای [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 است حذف میشود. این باید با موفقیت انجام شود. بقیه متد تأیید میکند که این امر واقعاً رخ داده است.
اجرای تستها نتایج زیر را تولید میکند:

تمام هفت تست با موفقیت انجام شد. ما لایه خود [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] (با کپی و پیست) کپی میکنیم:

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

این دو فایل را کپی میکنیم، سپس در اکلیپس آنها را در پوشه [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] است:

فایل [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] به شرح زیر است:

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

ما اضافه میکنیم یک شخص جدید از طریق لینک [Ajout] اضافه میکنیم:
![]() | ![]() |
ما افزودن را در پایگاه داده بررسی میکنیم:

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

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

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

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

کاربر U1 تغییرات را اعمال و ذخیره میکند:
![]() |
کاربر U2 همین کار را انجام میدهد:
![]() |
کاربر U2 از طریق لینک [Annuler] در فرم به فهرست افراد بازمیگردد:

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

نام شخص شمارهٔ ۸۹۹ در واقع پس از اصلاح انجامشده توسط 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 نیز با هر نسخه تغییر میکند
به جز این موارد، سایر موارد بدون تغییر باقی میمانند. در ادامه، این نسخههای جدید را با تمرکز صرف بر ویژگیهای جدیدی که هر یک معرفی میکنند، شرح میدهیم.

























