17. تطبيق الويب MVC في بنية ثلاثية الطبقات – المثال 3 – قاعدة بيانات Firebird
17.1. قاعدة بيانات Firebird
في هذا الإصدار الجديد، سنقوم بتثبيت قائمة الأشخاص في جدول قاعدة بيانات Firebird. ستجد في المستند [http://tahe.developpez.com/divers/sql-firebird/] معلومات حول تثبيت وإدارة هذا SGBD. فيما يلي، تأتي لقطات الشاشة من IBExpert، وهو عميل لإدارة قواعد بيانات Interbase وFirebird.
تسمى قاعدة البيانات [dbpersonnes.gdb]. وهي تحتوي على جدول [PERSONNES]:

ستحتوي الجدولة [PERSONNES] على قائمة بالأشخاص الذين تديرهم تطبيق الويب. وقد تم إنشاؤها باستخدام الأوامر التالية:
- السطور 2-10: تعكس بنية الجدول [PERSONNES]، المخصص لحفظ كائنات من النوع [Personne]، بنية هذا الكائن. نظرًا لعدم وجود النوع البولياني في Firebird، تم تعريف الحقل [MARIE] (السطر 8) على أنه من النوع [SMALLINT]، وهو عدد صحيح. وستكون قيمته 0 (غير متزوج) أو 1 (متزوج).
- السطور 13-16: قيود التكامل التي تعكس تلك الخاصة بمدقق البيانات [ValidatePersonne].
- السطر 19: الحقل ID هو المفتاح الأساسي للجدول [PERSONNES]
قد تحتوي الجدولة [PERSONNES] على المحتوى التالي:

تحتوي قاعدة البيانات [dbpersonnes.gdb]، بالإضافة إلى الجدول [PERSONNES]، على كائن يسمى المولد ويحمل الاسم [GEN_PERSONNES_ID]. يُصدر هذا المولد أرقامًا صحيحة متتالية سنستخدمها لإعطاء قيمتها للمفتاح الأساسي [ID] للفئة [PERSONNES]. لنأخذ مثالاً لتوضيح طريقة عمله:
![]() |
![]() |
يمكن ملاحظة أن قيمة المولد [GEN_PERSONNES_ID] قد تغيرت (انقر عليها مرتين + F5 للتحديث):
الترتيب SQL
مما يتيح الحصول على القيمة التالية للمولد [GEN_PERSONNES_ID]. GEN_ID هي وظيفة داخلية في Firebird و [RDB$DATABASE] هي جدول نظام لهذا SGBD.
17.2. مشروع Eclipse للطبقات [dao] و [service]
لتطوير طبقات [dao] و [service] لتطبيقنا الذي يعتمد على قاعدة بيانات، سنستخدم مشروع Eclipse [mvc-personnes-03] التالي:

المشروع هو مشروع Java بسيط، وليس مشروع ويب Tomcat. تذكر أن الإصدار 2 من تطبيقنا سيستخدم الطبقة [web] من الإصدار 1. لذلك لا داعي لكتابة هذه الطبقة.
مجلد [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]. هذه الفئة هي فئة الإصدار 1.
واجهة [IDao] هي كما يلي:
- تحتوي الواجهة على نفس الطرق الأربع الموجودة في الإصدار السابق.
وستكون الفئة [DaoImplCommon] التي تنفذ هذه الواجهة كما يلي:
- السطران 8-9: الفئة [DaoImpl] تنفذ الواجهة [IDao] وبالتالي الطرق الأربع [getAll, getOne, saveOne, deleteOne].
- السطور 27-37: تستخدم الطريقة [saveOne] طريقتين داخليتين هما [insertPersonne] و [updatePersonne] حسب ما إذا كان يجب إضافة شخص أو تعديله.
- السطر 50: الطريقة الخاصة [check] هي نفس الطريقة المستخدمة في الإصدار السابق. لن نعود إليها مرة أخرى.
- السطر 8: لتنفيذ واجهة [IDao]، تستمد الفئة [DaoImpl] من فئة Spring [SqlMapClientDaoSupport].
17.3.2. طبقة الوصول إلى البيانات [iBATIS]
تستخدم فئة Spring [SqlMapClientDaoSupport] إطار عمل تابع لجهة خارجية [Ibatis SqlMap] متاح على الرابط [http://ibatis.apache.org/]:

[iBATIS] هو مشروع Apache يسهل إنشاء طبقات [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]، فسيتعين عليه تكرار هذه السلسلة من الخطوات. العملية 3 هي الوحيدة الخاصة بطبقة [dao]، أما العمليات الأخرى فهي عامة. ستقوم فئة Spring [SqlMapClientDaoSupport] بنفسها بتنفيذ العمليات 1 و 2 و 4 و 5، وتفوض العملية 3 إلى فئتها المشتقة، وهي هنا فئة [DaoImplCommon].
لكي تعمل، تحتاج الفئة [SqlMapClientDaoSupport] إلى مرجع إلى الكائن iBATIS [SqlMapClient sqlMapClient] الذي سيتولى التواصل مع قاعدة البيانات. يحتاج هذا الكائن إلى شيئين لكي يعمل:
- كائن [DataSource] متصل بقاعدة البيانات والذي سيطلب منه الاتصالات
- ملف (أو ملفات) تكوين حيث يتم تضمين الأوامر SQL المطلوب تنفيذها. في الواقع، هذه الأوامر ليست موجودة في كود Java. يتم تحديدها بواسطة رمز في ملف التكوين ويستخدم الكائن [SqlMapClient sqlMapClient] هذا الرمز لتنفيذ أمر SQL معين.
فيما يلي نموذج أولي لتكوين طبقتنا [dao] الذي يعكس البنية المذكورة أعلاه:
<!-- فئات الوصول إلى الطبقة [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
هنا يتم تهيئة الخاصية [sqlMapClient] (السطر 3) للفئة [DaoImplCommon] (السطر 2). ويتم ذلك بواسطة الطريقة [setSqlMapClient] للفئة [DaoImpl]. هذه الفئة لا تحتوي على هذه الطريقة. بل إن فئتها الأم [SqlMapClientDaoSupport] هي التي تحتوي عليها. لذا فهي التي يتم تهيئتها هنا في الواقع.
الآن في السطر 4، نشير إلى كائن باسم "sqlMapClient" الذي لا يزال يتعين إنشاؤه. وكما ذكرنا، فإن هذا الكائن من النوع [SqlMapClient]، وهو نوع [iBATIS]:

[SqlMapClient] هي واجهة. يوفر Spring الفئة [SqlMapClientFactoryBean] للحصول على كائن ينفذ هذه الواجهة:

تذكر أننا نسعى إلى إنشاء مثيل لكائن ينفذ واجهة [SqlMapClient]. ويبدو أن هذا ليس هو الحال بالنسبة للفئة [SqlMapClientFactoryBean]. فهذه الفئة تنفذ الواجهة [FactoryBean] (انظر أعلاه). وتحتوي هذه الواجهة على الطريقة [getObject()] التالية:
![]()
عندما يُطلب من Spring مثيلًا لكائن يُنفذ الواجهة [FactoryBean]، فإنه:
- يُنشئ مثيل [I] من الفئة - هنا يُنشئ مثيلًا من النوع [SqlMapClientFactoryBean].
- يعيد إلى الطريقة المستدعية نتيجة الطريقة [I].getObject() - ستقوم الطريقة [SqlMapClientFactoryBean].getObject() هنا بإرجاع كائن ينفذ الواجهة [SqlMapClient].
لإرجاع كائن ينفذ واجهة [SqlMapClient]، تحتاج فئة [SqlMapClientFactoryBean] إلى معلومتين ضروريتين لهذا الكائن:
- كائن [DataSource] متصل بقاعدة البيانات والذي سيطلب منه الاتصالات
- ملف (أو ملفات) تكوين حيث يتم ترحيل أوامر SQL المطلوب تنفيذها
تحتوي الفئة [SqlMapClientFactoryBean] على طرق set لتهيئة هاتين الخاصيتين:

نحن نتقدم... يتضح ملف التكوين الخاص بنا ويصبح:
<!-- 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>
- السطران 2-3: الحبة " sqlMapClient " هي من النوع [SqlMapClientFactoryBean]. من ما تم شرحه للتو، نعلم أنه عندما نطلب من Spring مثيلًا لهذا bean، نحصل على كائن ينفذ واجهة iBATIS [SqlMapClient]. وهذا الكائن الأخير هو الذي سيتم الحصول عليه في السطر 14.
- الأسطر 7-9: نشير إلى أن ملف التكوين اللازم للكائن iBATIS [SqlMapClient] يسمى "sql-map-config-firebird.xml" ويجب البحث عنه في ClassPath الخاص بالتطبيق. يتم هنا استخدام الطريقة [SqlMapClientFactoryBean].setConfigLocation.
- الأسطر 4-6: نقوم بتهيئة الخاصية [dataSource] لـ [SqlMapClientFactoryBean] باستخدام طريقتها [setDataSource].
السطر 5، نشير إلى bean يسمى "dataSource" الذي لا يزال يتعين إنشاؤه. إذا نظرنا إلى المعلمة المتوقعة من قبل الطريقة [setDataSource] الخاصة بـ [SqlMapClientFactoryBean]، نرى أنها من النوع [DataSource]:

نحن نتعامل مرة أخرى مع واجهة يتعين علينا إيجاد فئة تنفيذ لها. وتتمثل مهمة هذه الفئة في تزويد التطبيق، بشكل فعال، باتصالات بقاعدة بيانات معينة. لا يمكن لـ SGBD الحفاظ على عدد كبير من الاتصالات مفتوحة في وقت واحد. لتقليل عدد الاتصالات المفتوحة في وقت معين، يتعين علينا، لكل تبادل مع قاعدة البيانات، القيام بما يلي:
- فتح اتصال
- بدء معاملة
- إصدار أوامر SQL
- إغلاق المعاملة
- إغلاق الاتصال
يستغرق فتح وإغلاق الاتصالات بشكل متكرر وقتًا طويلاً. لحل هاتين المشكلتين (الحد من عدد الاتصالات المفتوحة في وقت معين، والحد من تكلفة فتحها وإغلاقها)، غالبًا ما تتبع الفئات التي تنفذ واجهة [DataSource] الطريقة التالية:
- تفتح، فور إنشائها، N اتصالاً بالقاعدة البيانات المستهدفة. عادةً ما يكون لـ N قيمة افتراضية ويمكن في أغلب الأحيان تعريفها في ملف التكوين. ستبقى هذه الاتصالات N مفتوحة طوال الوقت وتشكل مجموعة من الاتصالات المتاحة لخيوط التطبيق.
- عندما يطلب مؤشر ترابط للتطبيق فتح اتصال، يمنحه الكائن [DataSource] أحد الاتصالات N المفتوحة عند بدء التشغيل، إذا كان لا يزال هناك اتصالات متاحة. عندما يغلق التطبيق الاتصال، لا يتم إغلاقه في الواقع بل يتم إعادته ببساطة إلى مجموعة الاتصالات المتاحة.
هناك العديد من تطبيقات واجهة [DataSource] المتاحة مجانًا. سنستخدم هنا تطبيق [commons DBCP] المتاح على الرابط [http://jakarta.apache.org/commons/dbcp/]:

يتطلب استخدام الأداة [commons DBCP] ملفين مضغوطين [commons-dbcp, commons-pool] تم وضعهما في مجلد [lib] الخاص بالمشروع:
![]() |
توفر فئة [BasicDataSource] من [commons DBCP] التنفيذ [DataSource] الذي نحتاجه:

ستوفر لنا هذه الفئة مجموعة من الاتصالات للوصول إلى قاعدة بيانات Firebird [dbpersonnes.gdb] لتطبيقنا. وللقيام بذلك، يجب تزويدها بالمعلومات التي تحتاجها لإنشاء اتصالات المجموعة:
- اسم برنامج التشغيل JDBC المطلوب استخدامه – تم تهيئته باستخدام [setDriverClassName]
- اسم عنوان 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>
- الأسطر 7-9: اسم برنامج التشغيل JDBC لـ SGBD Firebird
- الأسطر 11-13: عنوان URL لقاعدة بيانات Firebird [dbpersonnes.gdb]. يجب الانتباه بشكل خاص إلى كتابة هذا العنوان. يجب ألا يكون هناك أي مسافة بين العلامات <value> وعنوان URL.
- الأسطر 14-16: مالك الاتصال – هنا، [sysdba] وهو المسؤول الافتراضي لتوزيعات Firebird
- الأسطر 17-19: كلمة المرور الخاصة به [masterkey] – وهي أيضًا القيمة الافتراضية
لقد أحرزنا تقدماً كبيراً ولكن لا تزال هناك نقاط تكوين تحتاج إلى توضيح: يشير السطر 28 إلى الملف [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]. في Eclipse، يعني ذلك أنها ستكون موجودة عند التشغيل في المجلد [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> كعلامة جذر (السطران 6 و 8)
- السطر 7: تُستخدم العلامة <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#, #version#, #nom#, #prenom#, #dateNaissance#, #marie#,
#nbEnfants#) </insert>
<!-- تحديث شخص -->
<update id="Personne.updateOne" parameterClass="Personne.classe"> update
PERSONNES set VERSION=#الإصدار#+1، NOM=#اللقب#، PRENOM=#الاسم الأول#، DATENAISSANCE=#dateNaissance#,
MARIE=#marie#، NBENFANTS=#nbEnfants# WHERE ID=#id# و
VERSION=#version#</update>
<!-- حذف شخص -->
<delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE
ID=#value# </delete>
</sqlMap>
- يجب أن يكون للملف <sqlMap> كعلامة جذر (السطران 7 و 45)
- السطران 9-10: لتسهيل كتابة الملف، يتم إعطاء الاسم المستعار (المرادف) [Personne.classe] للفئة [istia.st.springmvc.personnes.entites.Personne].
- السطور 12-21: تحدد المطابقات بين أعمدة الجدول [PERSONNES] وحقول الكائن [Personne].
- السطور 23-24: الأمر SQL [select] للحصول على جميع الأشخاص من الجدول [PERSONNES]
- السطران 26-27: الأمر SQL [select] للحصول على شخص معين من الجدول [PERSONNES]
- السطور 29-36: الأمر SQL [insert] الذي يدرج شخصًا في الجدول [PERSONNES]
- الأسطر 38-41: الأمر SQL [update] الذي يقوم بتحديث شخص من الجدول [PERSONNES]
- السطور 42-44: الأمر SQL [delete] الذي يحذف شخصًا من الجدول [PERSONNES]
سيتم شرح دور ومغزى محتوى الملف [personnes-firebird.xml] من خلال دراسة الفئة [DaoImplCommon] التي تنفذ الطبقة [dao].
17.3.3. الفئة [DaoImplCommon]
لنعد إلى بنية الوصول إلى البيانات:
![]() |
الفئة [DaoImplCommon] هي كما يلي:
سنقوم بدراسة الطرق واحدة تلو الأخرى.
getAll
تسمح هذه الطريقة بالحصول على جميع الأشخاص الموجودين في القائمة. ورمزها هو التالي:
لنتذكر أولاً أن الفئة [DaoImplCommon] مشتقة من فئة Spring [SqlMapClientDaoSupport]. هذه الفئة هي التي تحتوي على الطريقة [getSqlMapClientTemplate()] المستخدمة في السطر 3 أعلاه. هذه الطريقة لها التوقيع التالي:
![]()
يغلف النوع [SqlMapClientTemplate] الكائن [SqlMapClient] من الطبقة [iBATIS]. ومن خلاله يمكن الوصول إلى قاعدة البيانات. يمكن استخدام النوع [iBATIS] SqlMapClient مباشرةً لأن الفئة [SqlMapClientDaoSupport] يمكنها الوصول إليه:
![]()
عيب الفئة [iBATIS] SqlMapClient هو أنها تطلق استثناءات من النوع [SQLException]، وهو نوع من الاستثناءات الخاضعة للرقابة، c.a.d. والتي يجب معالجتها بواسطة try / catch أو الإعلان عنها في توقيع الطرق التي تطلقها. لكن لنتذكر أن الطبقة [dao] تنفذ واجهة [IDao] التي لا تحتوي طرقها على استثناءات في توقيعاتها. وبالتالي، لا يمكن أن تحتوي طرق فئات تنفيذ واجهة [IDao] أيضًا على استثناءات في توقيعاتها. لذلك، يتعين علينا اعتراض كل استثناء [SQLException] يتم إطلاقه من قبل الطبقة [iBATIS] وتغليفه في استثناء غير خاضع للرقابة. سيكون النوع [DaoException] في مشروعنا مناسبًا لهذا التغليف.
بدلاً من إدارة هذه الاستثناءات بأنفسنا، سنعهد بها إلى نوع Spring [SqlMapClientTemplate] الذي يغلف الكائن [SqlMapClient] من الطبقة [iBATIS]. في الواقع، تم إنشاء [SqlMapClientTemplate] لاعتراض الاستثناءات [SQLException] التي تطلقها الطبقة [SqlMapClient] وتغليفها في نوع [DataAccessException] غير خاضع للرقابة. هذا السلوك يناسبنا. علينا فقط أن نتذكر أن الطبقة [dao] أصبحت الآن قادرة على إطلاق نوعين من الاستثناءات غير الخاضعة للرقابة:
- نوعنا الخاص [DaoException]
- نوع Spring [DataAccessException]
يتم تعريف النوع [SqlMapClientTemplate] على النحو التالي:

وهو ينفذ واجهة [SqlMapClientOperations] التالية:

تحدد هذه الواجهة طرقًا قادرة على استغلال محتوى الملف [personnes-firebird.xml]:
[queryForList]
![]()
تسمح هذه الطريقة بإصدار أمر [SELECT] واسترداد النتيجة في شكل قائمة من الكائنات:
- [statementName]: معرف (id) الأمر [select] في ملف التكوين
- [parameterObject]: الكائن "parameter" لـ [select] الذي تم تكوينه. يمكن أن يتخذ الكائن "parameter" شكلين:
- كائن يتوافق مع معيار 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]:
- السطر 4: يتم تنفيذ الأمر [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>
- السطر 23: الأمر SQL "Personne.getAll" غير معلم (عدم وجود معلمات في نص الاستعلام).
- السطر 3 من الطريقة [getAll] يطلب تنفيذ الاستعلام [select] المسمى "Personne.getAll". سيتم تنفيذ هذا الاستعلام. يعتمد [iBATIS] على JDBC. ومن ثم، نعلم أن نتيجة الاستعلام ستُحصل عليها في شكل كائن [ResultSet]. السطر 23، السمة [resultMap] لعلامة <select> تشير إلى [iBATIS] أي " resultMap " الذي يجب أن يستخدمه لتحويل كل سطر من كائن [ResultSet] الذي تم الحصول عليه. وهو " resultMap " [Personne.map] المحدد في الأسطر 12-21 الذي يشير إلى كيفية الانتقال من سطر في الجدول [PERSONNES] إلى كائن من النوع [Personne]. سيستخدم [iBATIS] هذه المطابقات لتوفير قائمة من كائنات [Personne] من أسطر كائن [ResultSet].
- ثم يعرض السطر 3 من الطريقة [getAll] مجموعة من الكائنات [Personne]
- يمكن أن تطلق الطريقة [queryForList] استثناء Spring [DataAccessException]. نتركها ترتفع.
سنشرح الطرق الأخرى للفئة [AbstractDaoImpl] بشكل أسرع، حيث تم توضيح الأساسيات حول استخدام [iBATIS] في دراسة الطريقة [getAll].
getOne
تسمح هذه الطريقة بالحصول على شخص محدد بواسطة رمزه [id]. رمزه هو التالي:
- السطر 4: يطلب تنفيذ الأمر [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=#القيمة#</select>
يتم تعيين الأمر SQL بواسطة المعلمة #value# (السطر 4). يشير السمة #value# إلى قيمة المعلمة التي تم تمريرها إلى الأمر SQL، عندما تكون هذه المعلمة من النوع البسيط: Integer، Double، String، ... في سمات العلامة <select>، تشير السمة [parameterClass] إلى أن المعلمة من النوع الصحيح (السطر 2). في السطر 5 من [getOne]، نرى أن هذا المعامل هو معرف الشخص المطلوب في شكل كائن Integer. هذا التغيير في النوع إلزامي لأن المعلمة الثانية لـ [queryForList] يجب أن تكون من النوع [Object].
سيتم تحويل نتيجة الاستعلام [select] إلى كائن عبر السمة [resultMap="Personne.map"] (السطر 2). وبالتالي سنحصل على نوع [Personne].
- السطور 7-11: إذا لم يرد أي سطر من الاستعلام [select]، يتم استرداد المؤشر null في السطر 4. وهذا يعني أنه لم يتم العثور على الشخص المطلوب. في هذه الحالة، يتم تشغيل [DaoException] برمز 2 (السطور 9-10).
- السطر 13: إذا لم تكن هناك استثناءات، يتم إرجاع الكائن [Personne] المطلوب.
deleteOne
تسمح هذه الطريقة بحذف شخص تم تحديده بواسطة [id]. رمزها هو التالي:
- السطران 4-5: يطلب تنفيذ الأمر [delete] المسمى "Personne.deleteOne". وهذا هو الأمر التالي في الملف [personnes-firebird.xml]:
<!-- حذف شخص -->
<delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE
ID=#القيمة# </delete>
يتم تعيين الأمر SQL بواسطة المعلمة #value# (السطر 3) من النوع [parameterClass="int"] (السطر 2). وسيكون هذا هو معرف الشخص المطلوب (السطر 5 من deleteOne)
- السطر 4: نتيجة طريقة [SqlMapClientTemplate].delete هي عدد الأسطر التي تم حذفها.
- السطران 7-8: إذا لم تحذف الاستعلام [delete] أي سطر، فهذا يعني أن الشخص غير موجود. يتم تشغيل [DaoException] برمز 2 (السطر 8).
saveOne
تسمح هذه الطريقة بإضافة شخص جديد أو تعديل شخص موجود. ورمزها هو التالي:
- السطر 4: يتم التحقق من صحة الشخص باستخدام الطريقة [check]. كانت هذه الطريقة موجودة بالفعل في الإصدار السابق وتم تعليقها آنذاك. يتم تشغيل [DaoException] إذا كان الشخص غير صالح. يتم السماح بظهور هذا الخطأ.
- السطر 6: إذا وصلنا إلى هنا، فهذا يعني أنه لم تكن هناك استثناءات. وبالتالي، فإن الشخص صالح.
- الأسطر 6-11: وفقًا لمعرف الشخص، إما أننا نتعامل مع إضافة (id= -1) أو تحديث (id<> -1). في كلتا الحالتين، يتم استدعاء طريقتين داخليتين للفئة:
- insertPersonne: للإضافة
- updatePersonne: للتحديث
insertPersonne
تسمح هذه الطريقة بإضافة شخص جديد. ورمزها هو التالي:
- السطر 4: نضع الرقم 1 في رقم إصدار الشخص الذي نقوم بإنشائه
- السطر 9: يتم الإدراج عبر الاستعلام المسمى "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#, #nom#, #prenom#, #dateNaissance#, #marie#,
#nbEnfants#) </insert>
هذه استعلام معلم، والمعلمة من النوع [Personne] (parameterClass="Personne.classe"، السطر 1). يتم استخدام حقول الكائن [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] في السطر 2 هي التي تشير إلى هذا الحقل.
- الاستعلام SQL الذي يجب تنفيذه للحصول على قيمة المفتاح الأساسي. الاستعلام المذكور هنا هو نفسه الذي عرضناه في الفقرة 17.1. تجدر الإشارة إلى نقطتين:
- السطران 6-7: لأغراض الاختبار، سنضطر إلى الانتظار لمدة 10 مللي ثانية قبل إجراء الإدراج، وذلك لمعرفة ما إذا كان هناك تعارض بين الخيوط التي ترغب في إجراء إضافات في نفس الوقت.
updatePersonne
تسمح هذه الطريقة بتعديل شخص موجود بالفعل في الجدول [PERSONNES]. ورمزها هو التالي:
- قد يفشل التحديث لسببين على الأقل:
- الشخص المراد تحديثه غير موجود
- الشخص المراد تحديثه موجود ولكن الخيط الذي يريد تعديله لا يمتلك الإصدار الصحيح
- السطران 7-8: يتم تنفيذ الاستعلام 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 ID=#id# و
VERSION=#version#</update>
- (تابع)
- السطر 2: يتم تعيين معلمات الطلب ويقبل كمعلمة نوع [Personne] (parameterClass="Personne.classe"). وهذا هو الشخص المراد تعديله (السطر 8 – updatePersonne).
- نريد تعديل الشخص الموجود في الجدول [PERSONNES] الذي يحمل نفس الرقم [id] ونفس الإصدار [version] مثل المعلمة. ولهذا السبب، لدينا القيد [WHERE ID=#id# and VERSION=#version#]. إذا تم العثور على هذا الشخص، يتم تحديثه باستخدام الشخص المعلمة ويتم زيادة إصداره بمقدار 1 (السطر 3 أعلاه).
- السطر 9: يتم استرداد عدد الأسطر التي تم تحديثها.
- السطران 10-11: إذا كان هذا العدد صفرًا، يتم تشغيل [DaoException] برمز 2، مما يشير إلى أن الشخص المراد تحديثه إما غير موجود أو أن إصداره قد تغير في غضون ذلك.
17.4. اختبارات طبقة [dao]
17.4.1. اختبارات تنفيذ [DaoImplCommon]
الآن بعد أن كتبنا الطبقة [dao]، نقترح اختبارها باستخدام اختبارات JUnit:

قبل إجراء اختبارات مكثفة، يمكننا البدء ببرنامج بسيط من النوع [main] الذي سيعرض محتوى الجدول [PERSONNES]. هذه هي الفئة [MainTestDaoFirebird]:
ملف التكوين [spring-config-test-dao-firebird.xml] للطبقة [dao]، المستخدم في السطور 13-14، هو التالي:
<?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.
لإجراء الاختبار، يتم تشغيل Firebird SGBD. محتوى الجدول [PERSONNES] هو كما يلي:

يؤدي تشغيل البرنامج [MainTestDaoFirebird] إلى ظهور النتائج التالية على الشاشة:

لقد حصلنا بالفعل على قائمة الأشخاص. يمكننا الانتقال إلى الاختبار JUnit.
اختبار JUnit [TestDaoFirebird] هو التالي:
- الاختبارات من [test1] إلى [test5] هي نفسها الموجودة في الإصدار 1، باستثناء [test4] الذي تطور قليلاً. أما الاختبار [test6] فهو جديد. لن نعلق إلا على هذين الاختبارين.
[test4]
يهدف [test4] إلى اختبار طريقة [updatePersonne - DaoImplCommon]. نذكر رمز هذه الطريقة:
- السطران 4-5: ننتظر 10 مللي ثانية. وبذلك نجبر الخيط الذي ينفذ [updatePersonne] على فقدان المعالج، مما قد يزيد من فرصنا في رؤية تعارضات الوصول بين الخيوط المتنافسة.
يُطلق [test4] N=100 خيطًا مكلفًا بزيادة عدد أبناء الشخص نفسه بمقدار 1 في نفس الوقت. نريد أن نرى كيف يتم إدارة تعارضات الإصدارات وتعارضات الوصول.
يتم إنشاء الخيوط في الأسطر 8-13. سيقوم كل منها بزيادة عدد أطفال الشخص الذي تم إنشاؤه في الأسطر 3-5 بمقدار 1. خيوط التحديث [ThreadDaoMajEnfants ] هي التالية:
قد يفشل تحديث الشخص لأن الشخص الذي نريد تعديله غير موجود أو لأنه تم تحديثه مسبقًا بواسطة مؤشر ترابط آخر. يتم التعامل مع هاتين الحالتين هنا في الأسطر 67-69. في هاتين الحالتين، تطلق الطريقة [updatePersonne] عملية [DaoException] برمز 2. ثم يعود الخيط لإعادة إجراء عملية التحديث من البداية (حلقة while، السطر 34).
[test6]
تهدف [test6] إلى اختبار الطريقة [insertPersonne - DaoImplCommon]. نذكر رمز هذه الطريقة:
- السطران 6-7: ننتظر 10 مللي ثانية لإجبار الخيط الذي ينفذ [insertPersonne] على فقدان المعالج وبالتالي زيادة فرص ظهور تعارضات بسبب الخيوط التي تقوم بإدخالات في نفس الوقت.
رمز [test6] هو التالي:
نقوم بإنشاء 100 مؤشر ترابط ستقوم بإدراج 100 شخص مختلف في نفس الوقت. ستحصل هذه المؤشرات الـ 100 جميعها على مفتاح أساسي للشخص الذي يجب إدراجه، ثم يتم مقاطعتها لمدة 10 مللي ثانية (السطر 10 – insertPersonne) قبل أن تتمكن من إجراء الإدراج. نريد التحقق من أن الأمور تسير على ما يرام، وبشكل خاص أن الخيوط تحصل على قيم مختلفة للمفتاح الأساسي.
- الأسطر 7-11: يتم إنشاء مصفوفة تضم 100 شخص. هؤلاء الأشخاص هم جميعًا نسخ من الشخص p الذي تم إنشاؤه في الأسطر 4-5.
- الأسطر 14-17: يتم تشغيل 100 مؤشر ترابط للإدراج. كل واحد منها مكلف بإدراج واحدة من الأشخاص الـ 100 الذين تم إنشاؤهم سابقًا.
- الأسطر 19-23: ينتظر [test6] انتهاء كل خيط من الخيوط المائة التي أطلقها. وعندما يكتشف انتهاء الخيط رقم i، يحذف الشخص الذي أدخله هذا الخيط للتو.
خيط الإدراج [ThreadDaoInsertPersonne] هو التالي:
- الأسطر 19-22: يقوم منشئ الخيط بتخزين الشخص الذي يجب إدراجه والطبقة [dao] التي يجب استخدامها لإجراء هذا الإدراج.
- السطر 30: يتم إدراج الشخص. إذا حدثت استثناء، يتم إرساله إلى [test6].
الاختبارات
عند إجراء الاختبارات، نحصل على النتائج التالية:
![]() |
وبالتالي، فشل الاختبار [test4]. ارتفع عدد العناصر الفرعية إلى 69 بدلاً من 100 كما كان متوقعاً. ماذا حدث؟ دعونا نلقي نظرة على سجلات الشاشة. تظهر هذه السجلات وجود استثناءات أطلقها 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.
--- حدث الخطأ أثناء تطبيق خريطة المعلمات.
--- تحقق من Personne.updateOne-InlineParameterMap.
--- تحقق من العبارة (فشل التحديث).
--- السبب: org.firebirdsql.jdbc.FBSQLException: استثناء GDS. 335544336. حالة تعطل
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:
--- حدث الخطأ في personnes-firebird.xml.
--- حدث الخطأ أثناء تطبيق خريطة المعلمات.
- السطر 1 – حدثت استثناء Spring [org.springframework.jdbc.UncategorizedSQLException]. وهو استثناء غير خاضع للرقابة تم استخدامه لتغليف استثناء أطلقه برنامج تشغيل Firebird JDBC، الموصوف في السطر 6.
- السطر 6 – أطلق برنامج تشغيل Firebird JDBC استثناءً من النوع [org.firebirdsql.jdbc.FBSQLException] ورمز الخطأ 335544336.
- السطر 7: يشير إلى حدوث تعارض في الوصول بين خيطين أرادا تحديث نفس السطر في الجدول [PERSONNES] في نفس الوقت.
هذه ليست خطأ لا يمكن إصلاحه. يمكن للخيط الذي يعترض هذا الاستثناء إعادة محاولة التحديث. وللقيام بذلك، يجب تعديل كود [ThreadDaoMajEnfants]:
- السطر 8: يتم التعامل مع استثناء من النوع [DaoException]. وفقًا لما قيل، يتعين علينا التعامل مع الاستثناء الذي ظهر في الاختبارات، وهو النوع [org.springframework.jdbc.UncategorizedSQLException]. ومع ذلك، لا يمكننا الاكتفاء بمعالجة هذا النوع الذي يعد نوعًا عامًا في Spring مخصصًا لتغليف الاستثناءات التي لا يعرفها. يعرف Spring الاستثناءات الصادرة عن برامج التشغيل JDBC لعدد من SGBD مثل Oracle و MySQL و Postgres، DB2، SQL Server، ... ولكن ليس Firebird. كما أن أي استثناء يتم إطلاقه بواسطة برنامج التشغيل JDBC الخاص بـ Firebird يتم تغليفه في نوع Spring [org.springframework.jdbc.UncategorizedSQLException]:

نرى أعلاه أن الفئة [UncategorizedSQLException] مشتقة من الفئة [DataAccessException] التي ذكرناها في الفقرة 17.3.3. من الممكن معرفة الاستثناء الذي تم تغليفه في [UncategorizedSQLException] بفضل طريقتها [getSQLException]:
![]()
هذا الاستثناء من النوع [SQLException] هو الذي أطلقته الطبقة [iBATIS] التي تغلف بدورها الاستثناء الذي أطلقه برنامج تشغيل قاعدة البيانات JDBC. يمكن الحصول على السبب الدقيق للاستثناء من النوع [SQLException] باستخدام الطريقة:
![]()
نحصل على الكائن من النوع [Throwable] الذي أطلقه برنامج التشغيل JDBC:

النوع [Throwable] هو الفئة الأم لـ [Exception].
هنا سيتعين علينا التحقق من أن الكائن من النوع [Throwable] الذي أطلقه برنامج التشغيل JDBC من Firebird والذي تسبب فيالاستثناء [SQLException] الذي أطلقته الطبقة [iBATIS] هو بالفعل استثناء من النوع [org.firebirdsql.gds.GDSException] ورمز الخطأ 335544336. لاسترداد رمز الخطأ، يمكننا استخدام الطريقة [getErrorCode()] من الفئة [org.firebirdsql.gds.GDSException].
إذا استخدمنا في كود [ThreadDaoMajEnfants] الاستثناء [org.firebirdsql.gds.GDSException]، فلن يتمكن هذا الخيط من العمل إلا مع Firebird SGBD. وينطبق الأمر نفسه على الاختبار [test4] الذي يستخدم هذا الخيط. نريد تجنب ذلك. في الواقع، نرغب في أن تظل اختباراتنا JUnit صالحة بغض النظر عن SGBD المستخدم. لتحقيق هذه النتيجة، قررنا أن الطبقة [dao] ستطلق [DaoException] برمز 4 عند اكتشاف استثناء من نوع "تعارض التحديث"، وذلك بغض النظر عن SGBD الأساسي. وبالتالي، يمكن إعادة كتابة مؤشر الترابط [ThreadDaoMajEnfants] على النحو التالي:
- الأسطر 34-36: يتم اعتراض الاستثناء من النوع [DaoException] ذي الرمز 4. سيُجبر مؤشر الترابط [ThreadDaoMajEnfants] على إعادة إجراء عملية التحديث من البداية (السطر 10)
لذلك يجب أن تكون طبقتنا [dao] قادرة على التعرف على استثناء من نوع "تعارض التحديث". يتم إصدار هذا الاستثناء بواسطة برنامج تشغيل JDBC وهو خاص به. يجب معالجة هذا الاستثناء في الطريقة [updatePersonne] للفئة [DaoImplCommon]:
يجب أن تكون الأسطر 7-11 محاطة بـ try / catch. بالنسبة لـ SGBD Firebird، علينا التحقق من أن الاستثناء الذي تسبب في فشل التحديث هو من النوع [org.firebirdsql.gds.GDSException] وأن رمز الخطأ الخاص به هو 335544336. إذا وضعنا هذا النوع من الاختبار في [DaoImplCommon]، فسوف نربط هذه الفئة بـ SGBD Firebird، وهو أمر غير مرغوب فيه بالطبع. إذا أردنا الحفاظ على الطابع العام للفئة [DaoImplCommon]، فيجب علينا اشتقاقها وإدارة الاستثناء في فئة خاصة بـ Firebird. وهذا ما سنفعله الآن.
17.4.2. الفئة [DaoImplFirebird]
رمزها هو التالي:
- السطر 5: الفئة [DaoImplFirebird] مشتقة من [DaoImplCommon]، الفئة التي درسناها للتو. وهي تعيد تعريف، في الأسطر 8-33، الطريقة [updatePersonne] التي تسبب لنا مشكلة.
- السطر 20: نلتقط استثناء Spring من النوع [UncategorizedSQLException]
- السطور 21-22: نتحقق من أن الاستثناء الأساسي من النوع [SQLException] والذي تم إطلاقه بواسطة الطبقة [iBATIS] ناتج عن استثناء من النوع [org.firebirdsql.jdbc.FBSQLException]
- السطر 25: نتحقق أيضًا من أن رمز خطأ هذا الاستثناء في Firebird هو 335544336، وهو رمز خطأ "التعطل".
- السطران 26-27: إذا تم استيفاء جميع هذه الشروط، يتم إطلاق [DaoException] برمز 4.
- السطور 36-44: تسمح طريقة [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>
- السطر 32: التنفيذ الجديد [DaoImplFirebird] للطبقة [dao].
نتائج اختبار [test4] الذي فشل سابقًا هي كما يلي:

تم اجتياز [test4] بنجاح. فيما يلي الأسطر الأخيرة من سجلات الشاشة:
يشير السطر الأخير إلى أن الخيط رقم 36 هو الذي انتهى أخيرًا. يظهر السطر 3 تعارضًا في الإصدار أجبر الخيط رقم 36 على استئناف إجراء تحديث الشخص (السطر 4). تظهر سجلات أخرى تعارضات في الوصول أثناء عمليات التحديث:
يُظهر السطر 2 أن الخيط رقم 75 قد فشل أثناء التحديث بسبب تعارض في التحديث: عندما تم إصدار الأمر SQL [update] على الجدول [PERSONNES]، كان السطر المطلوب تحديثه مقفلاً بواسطة مؤشر ترابط آخر. سيجبر تعارض الوصول هذا مؤشر الترابط رقم 75 على إعادة محاولة التحديث.
وللانتهاء من [test4]، نلاحظ اختلافًا ملحوظًا عن نتائج الاختبار نفسه في الإصدار 1 حيث فشل بسبب مشاكل في التزامن. نظرًا لعدم تزامن طرق الطبقة [dao] في الإصدار 1، ظهرت تعارضات في الوصول. هنا، لم نحتاج إلى مزامنة الطبقة [dao]. لقد قمنا ببساطة بإدارة تعارضات الوصول التي أبلغ عنها Firebird.
لنقم الآن بتنفيذ الاختبار الكامل JUnit للطبقة [dao]:

يبدو إذن أن لدينا طبقة [dao] صالحة. وللتأكد من صلاحيتها بدرجة عالية من الاحتمال، يتعين علينا إجراء المزيد من الاختبارات. ومع ذلك، سنعتبرها جاهزة للعمل.
17.5. الطبقة [service]
17.5.1. مكونات الطبقة [service]
تتكون الطبقة [service] من الفئات والواجهات التالية:
![]()
- [IService] هي الواجهة التي تقدمها الطبقة [service]
- [ServiceImpl] هو تطبيق لهذه الواجهة
الواجهة [IService] هي كما يلي:
- تحتوي الواجهة على نفس الطرق الأربع الموجودة في الإصدار 1، ولكنها تحتوي على طريقتين إضافيتين:
- saveMany: تسمح بحفظ عدة أشخاص في نفس الوقت بشكل متكامل. إما أن يتم حفظهم جميعًا، أو لا يتم حفظ أي منهم.
- deleteMany: تسمح بحذف عدة أشخاص في نفس الوقت بشكل متكامل. إما أن يتم حذفهم جميعًا، أو لا يتم حذف أي منهم.
لن يتم استخدام هاتين الطريقتين من قبل تطبيق الويب. لقد أضفناهما لتوضيح مفهوم المعاملة في قاعدة البيانات. يجب تنفيذ الطريقتين ضمن معاملة واحدة للحصول على الترابط المطلوب.
ستكون الفئة [ServiceImpl] التي تنفذ هذه الواجهة كما يلي:
- تستدعي الطرق [getAll, getOne, insertOne, saveOne] الطرق ذات الاسم نفسه في الطبقة [dao].
- الأسطر 42-47: تقوم الطريقة [saveMany] بحفظ الأشخاص الموجودين في المصفوفة التي تم تمريرها كمعلمة، واحدًا تلو الآخر.
- الأسطر 50-55: تقوم الطريقة [deleteMany] بحذف الأشخاص، واحدًا تلو الآخر، الذين تم تمرير الجدول الخاص بهم من id كمعلمة
لقد ذكرنا أن الطريقتين [saveMany] و [deleteMany] يجب أن تتما ضمن معاملة واحدة لضمان مبدأ "كل شيء أو لا شيء" لهذه الطرق. يمكننا ملاحظة أن الكود أعلاه يتجاهل تمامًا مفهوم المعاملة هذا. لن تظهر هذه المعاملة إلا في ملف تكوين الطبقة [service].
17.5.2. تكوين الطبقة [service]
في السطر 11 أعلاه، نرى أن التنفيذ [ServiceImpl] يحتوي على مرجع إلى الطبقة [dao]. وستتم تهيئة هذه الطبقة، كما في الإصدار 1، بواسطة Spring عند إنشاء مثيل للطبقة [service - ServiceImpl]. وسيكون ملف التكوين الذي سيسمح بإنشاء مثيل للطبقة [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>
- الأسطر 1-36: تكوين الطبقة [dao]. تم شرح هذا التكوين عند دراسة الطبقة [dao] في الفقرة 17.3.2.
- الأسطر 38-64: تكوين الطبقة [service]
في السطر 46، يمكن ملاحظة أن تنفيذ الطبقة [service] يتم بواسطة النوع [TransactionProxyFactoryBean]. كنا نتوقع العثور على النوع [ServiceImpl]. [TransactionProxyFactoryBean] هو نوع محدد مسبقًا في Spring. كيف يمكن لنوع محدد مسبقًا أن ينفذ واجهة [IService] التي هي، بدورها، خاصة بتطبيقنا؟
لنكتشف أولاً الفئة [TransactionProxyFactoryBean]:

نرى أنها تنفذ واجهة [FactoryBean]. لقد سبق أن تعرفنا على هذه الواجهة. ونعلم أنه عندما يطلب تطبيق ما من Spring مثيلًا لنوع ينفذ [FactoryBean]، لا يعرض Spring مثيل [I] من هذا النوع، بل الكائن الذي تعرضه الطريقة [I].getObject() :
![]()
في حالتنا، سيتم تنفيذ الطبقة [service] بواسطة الكائن الذي يتم إرجاعه بواسطة [TransactionProxyFactoryBean].getObject(). ما هي طبيعة هذا الكائن؟ لن ندخل في التفاصيل لأنها معقدة. فهي تندرج تحت ما يُسمى Spring AOP (البرمجة الموجهة للجانب). سنحاول توضيح الأمور باستخدام مخططات بسيطة. يتيح AOP ما يلي:
- لدينا فئتان C1 و C2، حيث تستخدم C1 الواجهة [I2] التي تقدمها C2:
![]() |
- بفضل AOP، يمكن وضع معترض بين الفئتين C1 و C2 بطريقة شفافة بالنسبة للفئتين:
![]() |
تم تجميع الفئة [C1] للعمل مع الواجهة [I2] التي تنفذها [C2]. عند التنفيذ، تقوم AOP بوضع الفئة [intercepteur] بين [C1] و [C2]. ولكي يكون ذلك ممكنًا، يجب بالطبع أن تقدم الفئة [intercepteur] إلى [C1] نفس الواجهة [I2] التي تقدمها [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] التي قد تضيف شيئًا إلى ما تم تنفيذه في الخطوة 2.
- تُرجع الطريقة 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 في هذه الحالة ضمن المعاملة التي بدأت في الخطوة 2.
- لنفترض أن إحدى هذه العمليات فشلت. ستسمح الطبقة [dao] بترحيل استثناء إلى الطبقة [service]، وهي في هذه الحالة الطريقة [saveMany] للمثيل [ServiceImpl].
- لا تقوم هذه الطريقة بأي شيء وتسمح بترحيل الاستثناء إلى الطريقة [saveMany] الخاصة بـ [proxy transactionnel].
- عند استلام الاستثناء، تقوم الطريقة [saveMany] التابعة لـ [proxy transactionnel]، وهي المالكة للمعاملة، بإجراء [rollback] عليها لإلغاء جميع التحديثات، ثم تترك الاستثناء يرتفع إلى الطبقة [web] التي ستكون مسؤولة عن إدارته.
في الخطوة 4، افترضنا أن إحدى عمليات الإدراج أو التحديث قد فشلت. إذا لم يكن الأمر كذلك، فلن يتم رفع أي استثناء في [5]. وينطبق الأمر نفسه على [6]. في هذه الحالة، تقوم الطريقة [saveMany] في [proxy transactionnel] بإجراء [commit] للمعاملة للتحقق من صحة جميع عمليات التحديث.
لدينا الآن فكرة أكثر دقة عن البنية التي أنشأتها الحبة [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] إلى مرجع إلى مدير المعاملات المختار.
- الأسطر 11 – 13: تحدد السمة [transactionManager] للفول [TransactionProxyFactoryBean] مع مرجع إلى مدير المعاملات. يتم تحديد هذا في الأسطر 2 – 7.
- الأسطر 2-7: مدير المعاملات هو من النوع [DataSourceTransactionManager]:

[DataSourceTransactionManager] هو مدير معاملات مخصص لـ SGBD الذي يتم الوصول إليه عبر كائن [DataSource]. وهو لا يستطيع إدارة سوى المعاملات على كائن SGBD واحد. ولا يستطيع إدارة المعاملات الموزعة على عدة كائنات SGBD. وهنا، لدينا كائن SGBD واحد فقط. لذلك فإن مدير المعاملات هذا مناسب. عندما يبدأ [proxy transactionnel] معاملة، فإنه سيفعل ذلك على اتصال مرتبط بالخيط. هذا الاتصال هو الذي سيُستخدم في جميع الطبقات المؤدية إلى قاعدة البيانات: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].
تحتاج الفئة [DataSourceTransactionManager] إلى معرفة مصدر البيانات الذي يجب أن تطلب منه اتصالاً لربطه بالخيط. يتم تعريف هذا المصدر في الأسطر 4-6: وهو نفس مصدر البيانات المستخدم من قبل الطبقة [dao] (انظر الفقرة 17.5.2).
- الأسطر 14-19: يشير السمة "target" إلى الفئة التي يجب اعتراضها، وهي هنا الفئة [ServiceImpl]. هذه المعلومات ضرورية لسببين:
- يجب إنشاء مثيل للفئة [ServiceImpl] لأنها هي التي تضمن الحوار مع الطبقة [dao]
- يجب أن تقوم [TransactionProxyFactoryBean] بإنشاء وكيل يقدم للطبقة [web] نفس واجهة [ServiceImpl].
- الأسطر 21-27: تشير إلى طرق [ServiceImpl] التي يجب على الوكيل اعتراضها. يشير السمة [transactionAttributes]، في السطر 21، إلى طرق [ServiceImpl] التي تتطلب معاملة وإلى سمات هذه المعاملة:
- السطر 23: يتم تنفيذ الطرق التي يبدأ اسمها بـ get [getOne, getAll] في معاملة السمة [PROPAGATION_REQUIRED,readOnly]:
- PROPAGATION_REQUIRED: يتم تنفيذ الطريقة في معاملة إذا كانت هناك معاملة مرفقة بالفعل بالخيط، وإلا يتم إنشاء معاملة جديدة ويتم تنفيذ الطريقة فيها.
- readOnly: معاملة للقراءة فقط
هنا، ستُنفَّذ الطريقتان [getOne] و [getAll] من [ServiceImpl] في معاملة، في حين أن ذلك ليس ضروريًا في الواقع. في كل مرة، يتعلق الأمر بعملية تتكون من أمر واحد SELECT. لا نرى فائدة من وضع هذا SELECT في معاملة.
- السطر 24: يتم تنفيذ الطرق التي يبدأ اسمها بـ save، [saveOne, saveMany]، في معاملة السمة [PROPAGATION_REQUIRED].
- السطر 25: يتم تكوين الطرق [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 | |
- الأسطر 19-22: يختبر البرنامج الطبقات [dao] و [service] التي تم تكوينها بواسطة الملف [spring-config-test-service-firebird.xml]، الذي تمت دراسته في القسم السابق.
- الاختبارات من [test1] إلى [test6] متطابقة في جوهرها مع نظيراتها التي تحمل نفس الاسم في فئة الاختبار [TestDaoFirebird] للطبقة [dao]. والفرق الوحيد هو أنه حسب التكوين، يتم الآن تنفيذ الطريقتين [saveOne] و [deleteOne] في معاملة واحدة.
- تهدف الطريقة [test7] إلى اختبار الطريقتين [saveMany] و [deleteMany]. نريد التحقق من أنهما تعملان بشكل صحيح في معاملة. لنعلق على كود هذه الطريقة:
- السطور 62-63: نحسب عدد الأشخاص [nbPersonnes1] الموجودين حاليًا في القائمة
- السطور 67-72: يتم إنشاء ثلاثة أشخاص
- السطور 73-83: يتم حفظ هذه الأشخاص الثلاثة بواسطة الطريقة [saveMany] – السطر 77. سيتم إضافة الشخصين الأولين p1 و p2 اللذين لهما معرف يساوي -1 إلى الجدول [PERSONNES]. أما الشخص p3، فمعرفه يساوي -2. لذا، لا يتعلق الأمر بإدراج بل بتحديث. وسيفشل هذا التحديث لأنه لا يوجد أي شخص بمعرف يساوي -2 في الجدول [PERSONNES]. وبالتالي، ستطلق الطبقة [dao] استثناءً سيصل إلى الطبقة [service]. يتم اختبار وجود هذا الاستثناء في السطر 83.
- بسبب الاستثناء السابق، يجب أن تقوم الطبقة [service] بتحويل جميع أوامر SQL التي تم إصدارها أثناء تنفيذ الطريقة [saveMany] إلى [rollback]، وذلك لأن هذه الطريقة تُنفَّذ في معاملة. في السطور 86-87، نتحقق من أن عدد الأشخاص في القائمة لم يتغير، وبالتالي لم تتم عمليات الإدراج لـ p1 و p2.
- السطور 88-103: نضيف الشخصين p1 و p2 فقط ونتحقق من أن لدينا بعد ذلك شخصين إضافيين في القائمة.
- السطور 106-114: يتم حذف مجموعة من الأشخاص تتكون من p1 و p2 اللذين تمت إضافتهما للتو وشخص غير موجود (id= -1). يتم استخدام الطريقة [deleteMany] لهذا الغرض، السطر 108. ستفشل هذه الطريقة لأنه لا يوجد أي شخص برقم تعريف يساوي – 1 في الجدول [PERSONNES]. وبالتالي، ستطلق الطبقة [dao] استثناءً سيصل إلى الطبقة [service]. يتم اختبار وجود هذا الاستثناء في السطر 114.
- بسبب الاستثناء السابق، يجب أن تقوم الطبقة [service] بإنشاء [rollback] من مجموعة أوامر SQL الصادرة أثناء تنفيذ الطريقة [deleteMany]، وذلك لأن هذه الطريقة تُنفَّذ في معاملة. في السطرين 116-117، يتم التحقق من أن عدد الأشخاص في القائمة لم يتغير، وبالتالي لم يتم حذف p1 و p2.
- السطر 122: يتم حذف مجموعة تتكون فقط من الشخصين p1 و p2. ومن المفترض أن ينجح ذلك. ويتحقق باقي الأسلوب من صحة ذلك.
يؤدي تنفيذ الاختبارات إلى النتائج التالية:

تم اجتياز الاختبارات السبعة بنجاح. سنعتبر طبقتنا [service] جاهزة للعمل.
17.7. الطبقة [web]
لنتذكر البنية العامة لتطبيق الويب المراد إنشاؤه:
![]() |
لقد أنشأنا للتو الطبقتين [dao] و [service] اللتين تسمحان بالعمل مع قاعدة بيانات Firebird. لقد كتبنا الإصدار 1 من هذا التطبيق حيث كانت الطبقات [dao] و [service] تعمل مع قائمة بالأشخاص المخزنة في الذاكرة. تظل الطبقة [web] التي تمت كتابتها في هذه المناسبة صالحة. في الواقع، كانت موجهة إلى طبقة [service] التي تنفذ واجهة [IService]. وبما أن الطبقة الجديدة [service] تنفذ هذه الواجهة نفسها، فلا داعي لتعديل الطبقة [web].
في المقالة السابقة، تم اختبار الإصدار 1 من التطبيق مع مشروع Eclipse [mvc-personnes-02B] حيث تم وضع الطبقات [web, service, dao, entites] في أرشيفات .jar:
![]() |
كانت المجلد [src] فارغًا. كانت فئات الطبقات موجودة في أرشيفات [personnes-*.jar ]:
![]() |
لاختبار الإصدار 2، نقوم في Eclipse بنسخ المجلد Eclipse [mvc-personnes-02B] إلى [mvc-personnes-03B] (نسخ / لصق):

في المشروع [mvc-personnes-03]، نقوم بتصدير [File / Export / Jar file] والطبقات [dao] و [service] على التوالي إلى الأرشيفات [personnes-dao.jar] و [personnes-service.jar] من المجلد [dist] الخاص بالمشروع:

نقوم بنسخ هذين الملفين، ثم نلصقهما في Eclipse في المجلد [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>
- السطر 12: عنوان URL لقاعدة بيانات Firebird. نستمر في استخدام القاعدة التي استُخدمت لاختبار الطبقات [dao] و [service]
نقوم بنشر مشروع الويب [mvc-personnes-03B] داخل Tomcat:
![]() | ![]() |
نحن جاهزون لاختبارات . تم تشغيل Firebird SGBD. ويكون محتوى الجدول [PERSONNES] كما يلي:

يتم تشغيل Tomcat بدوره. باستخدام متصفح، نطلب عنوان URL [http://localhost:8080/mvc-personnes-03B]:

نضيف شخصًا جديدًا عبر الرابط [Ajout]:
![]() | ![]() |
نتحقق من الإضافة في قاعدة البيانات:

يُطلب من القارئ إجراء اختبارات أخرى [modification, suppression].
لنقم الآن باختبار تعارض الإصدارات الذي تم إجراؤه في الإصدار 1. سيكون [Firefox] هو متصفح المستخدم U1. يطلب هذا المتصفح عنوان URL [http://localhost:8080/mvc-personnes-03B]:

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

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

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

يقوم المستخدم U1 بإجراء تعديلات والتحقق من صحتها:
![]() |
المستخدم U2 يفعل الشيء نفسه:
![]() |
يعود المستخدم U2 إلى قائمة الأشخاص باستخدام الرابط [Annuler] الخاص بالنموذج:

ويجد الشخص [Perrichon] كما عدّله U1 (تحويل الاسم إلى أحرف كبيرة).
وماذا عن قاعدة البيانات في كل هذا؟ لنلقِ نظرة:

تم كتابة اسم الشخص رقم 899 بأحرف كبيرة بعد التعديل الذي أجراه U1.
17.8. الخلاصة
لنتذكر ما كنا نريد القيام به. كان لدينا تطبيق ويب بهيكل ثلاثي الطبقات كما يلي:
حيث كانت الطبقتان [dao] و [service] تعملان بقائمة بيانات في الذاكرة، والتي كانت تُفقد عند إيقاف تشغيل خادم الويب. كانت هذه هي النسخة 1. في النسخة 2، تمت إعادة كتابة الطبقتين [service] و [dao] بحيث تكون قائمة الأشخاص في جدول قاعدة بيانات. وبالتالي أصبحت الآن دائمة. نقترح الآن أن نرى تأثير تغيير SGBD على تطبيقنا. وللقيام بذلك، سنقوم بإنشاء ثلاث إصدارات جديدة من تطبيق الويب الخاص بنا:
![]() |
- الإصدار 3: SGBD هو Postgres
- الإصدار 4: SGBD هو MySQL
- الإصدار 5: 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 مع كل إصدار
بخلاف هذه النقاط، يبقى كل شيء على حاله. في ما يلي، نصف هذه الإصدارات الجديدة مع التركيز فقط على الميزات الجديدة التي يقدمها كل منها.

























