3. مقدمة إلى API JDBC
3.1. إعداد بيئة العمل
سنعمل مع قاعدة بيانات MySQL5.
يجب أن يكون لديك:
- تثبيت JDK (Java Development Kit) (الفقرة 23.1)؛
- تثبيت مدير التبعيات Maven (الفقرة 23.2)؛
- تثبيت مجموعة أدوات Spring (IDE) (STS) (الفقرة 23.3)؛
- تثبيت SGBD MySQL5 (الفقرة 23.4) وعميله EMS MyManager (الفقرة 23.5)؛
- تنزيل أكواد المستند [http://tahe.developpez.com/java/spring-database]؛
نفترض فيما يلي أن مسؤول MySQL5 هو root وكلمة المرور هي root. قم بتشغيل SGBD وMySQL5 وعميله [MyManager]. باستخدام [MyManager]، نقوم بإنشاء قاعدة البيانات [dbproduits] و [1-34]:
![]() |
- في [3]، يجب أن يُسمى الملف الأساسي [dbproduits]؛
![]() |
- في [8-9]، قم بتسجيل الدخول كـ root باستخدام كلمة مرور root (وهو ما لا تظهره لقطة الشاشة أعلاه)؛
![]() |
- إلى [14a]، وكلمة المرور هي root مرة أخرى (وهو ما لا تظهره لقطة الشاشة)؛
- في [15]، تم إنشاء قاعدة البيانات [dbproduits]؛
![]() |
- في [20]، انتبه إلى القاعدة المحددة. يجب أن تكون القاعدة [dbproduits]؛
![]() |
- إلى [22]، والمجلد هو <exemples>/spring-database-config/mysql/databases حيث <exemples> هو مجلد الأمثلة التي تم تنزيلها؛
- في [23]، حدد البرنامج النصي SQL [dbproduits.sql]. سيقوم هذا البرنامج النصي بإنشاء الجدول [PRODUITS] في قاعدة البيانات [dbproduits]؛
![]() |
![]() |
- في [30]، تم إنشاء الجدول [produits]؛
![]() |
- في [33]، أعمدة الجدول [produits]؛
![]() |
- إلى [34]، وهي فارغة في البداية؛
الآن باستخدام STS، قم باستيراد المشاريع التالية (اتبع نفس الخطوات المستخدمة لمشاريع المجلد <exemples>/spring-core):
![]() |
- في [2]، سيتم العثور على المشروع [mysql-config-jdbc] في المجلد [<exemples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1]؛
يُكوّن هذا المشروع الطبقة JDBC في البنية التالية:
![]() |
ثم قم باستيراد المشاريع الثلاثة التالية مرة أخرى:
![]() |
- إلى [2]، وستُوجد المشاريع في المجلد [<exemples>/spring-database-config/spring-jdbc] [1]؛
هذه المشاريع الثلاثة هي مشاريع Maven تستخدم مشروع Maven [mysql-config-jdbc]. ويقوم هذا المشروع الأخير بإنشاء عنصر Maven التالي (انظر pom.xml):
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
سيتم إنشاء نفس الأرتيفاكت بواسطة المشروع [oracle-config-jdbc, db2-config-jdbc, ...]. للتأكد من أن مشاريع [spring-generic-jdbc-*] المحملة حاليًا في STS تستخدم بالفعل المشروع [mysql-config-jdbc]:
- تأكد من عدم تحميل مشروع آخر باسم [sgbd-config-jdbc] في نفس الوقت. فقد يتسبب ذلك في حدوث أخطاء يصعب فهمها؛
- قم بتحديث إعدادات Maven للمشاريع المحملة على النحو التالي:
![]() |
![]() |
للتحقق من إعداداتك، قم بتشغيل إعدادات التشغيل [spring-jdbc-generic-01.IntroJdbc01] و [1-3]:
![]() |
يجب أن تحصل على النتائج التالية في وحدة التحكم:
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
{"id":1,"nom":"NOM1","categorie":1,"prix":110.00000000000001,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.10000000000001,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.2,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.30000000000001,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire :
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
------------------------------ Travail terminé
في الأمثلة التالية، يمكن للقارئ:
- إما العمل مباشرةً مع المشاريع التي تم تحميلها مسبقًا؛
- أو إنشاء المشاريع بنفسه؛
3.2. خطوات تشغيل قاعدة البيانات
![]() |
في البنية المذكورة أعلاه، يتضمن تشغيل قاعدة البيانات بواسطة برنامج وحدة التحكم الخطوات التالية:
- تحميل برنامج التشغيل JDBC الخاص بقاعدة البيانات؛
- فتح اتصال بالقاعدة؛
- إصدار الأمر SQL على قاعدة البيانات ومعالجة نتائج الأمر SQL؛
- إغلاق الاتصال؛
تُنفذ الخطوة 1 مرة واحدة فقط. أما الخطوات من 2 إلى 4 فتُنفذ بشكل متكرر. وتجدر الإشارة إلى أنه لا يُترك أي اتصال مفتوحًا، بل يُغلق فور انتهاء الحاجة إليه.
3.2.1. الخطوة 1 - تحميل برنامج التشغيل JDBC في الذاكرة
الرمز
// تحميل برنامج التشغيل JDBC
try {
Class.forName(nom de la classe du pilote JDBC);
} catch (ClassNotFoundException e1) {
// معالجة الاستثناء
}
تهدف العملية الواردة في السطر 3 إلى تحميل برنامج التشغيل JDBC من قاعدة البيانات إلى الذاكرة. لا يلزم إجراء هذه العملية إلا مرة واحدة. ومع ذلك، فإن تكرارها لا يتسبب في حدوث أي خطأ. يتم البحث عن فئة برنامج التشغيل JDBC في مسار الفئات (Classpath) الخاص بالمشروع. لذلك، يجب أن يكون ملف [jar] الذي يحتوي على فئة برنامج التشغيل JDBC قد أُدرج في مسار الفئات (Classpath) للمشروع في Eclipse.
3.2.2. الخطوة 2 - فتح اتصال
بمجرد تثبيت برنامج التشغيل JDBC، يُطلب منه فتح اتصال مع BD:
الكود
package spring.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class IntroJdbc01 {
...
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// فتح الاتصال
connexion = DriverManager.getConnection(url, user, passwd);
...
} catch (SQLException e1) {
// جاري معالجة الاستثناء
...
} finally {
// إغلاق الاتصال
if (connexion != null) {
try {
connexion.close();
} catch (SQLException e2) {
// معالجة الاستثناء
...
}
}
}
- الأسطر 3-7: توجد جميع فئات تنفيذ واجهة JDBC في الحزمة [java.sql]. كما أنها، في حالة حدوث خطأ، تطلق جميعها استثناءً من النوع [SQLException] (السطر 19، 27). هذا الاستثناء مشتق من الفئة [Exception] وهو ما يُعرف بالاستثناء «المُحكَم»: حيث يتعين استخدام عبارة try / catch لمعالجته، أو بدلاً من ذلك عدم معالجته والإشارة إلى أن الأسلوب يسمح بمرور الاستثناء عن طريق استكمال توقيع الأسلوب بـ [throws SQLException]؛
- السطر 17، [DriverManager.getConnection] هي دالة ثابتة تتوقع ثلاثة معلمات:
- [url]: قيمة URL الموجودة في قاعدة البيانات. وهي سلسلة أحرف تعتمد على قيمة BD المستخدمة. بالنسبة لـ MySQL، تكون على شكل [jdbc:mysql://localhost:3306/nom_de_la_bd]؛
- [user]: مالك الاتصال؛
- [passwd]: كلمة مروره؛
- الأسطر 24-30: يجب إغلاق الاتصال في الجملة [finally] بحيث يتم إغلاقه سواء حدث استثناء أم لا.
3.2.3. الخطوة 3 - إصدار الأوامر SQL و [SELECT]
بمجرد الحصول على اتصال، يمكن إصدار الأوامر SQL. تختلف طريقة إدارة أوامر القراءة [SELECT] عن تلك المستخدمة في عمليات التحديث [UPDATE, INSERT, DELETE]. نبدأ بأوامر SQL و [SELECT]:
الرمز
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// فتح الاتصال
connexion = DriverManager.getConnection(url, user, passwd);
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة فقط
connexion.setReadOnly(true);
// قراءة الجدول [PRODUITS]
ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
rs = ps.executeQuery();
System.out.println("Liste des produits : ");
while (rs.next()) {
System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
// تثبيت المعاملة
connexion.commit();
} catch (SQLException e1) {
// معالجة الاستثناء
doCatchException(connexion,e1);
} finally {
// معالجة جملة finally
doFinally(rs, ps, connexion);
}
private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
- السطران 8 و10: فتح معاملة (السطر 8) في وضع القراءة فقط (السطر 10). المعاملة هي سلسلة من الأوامر SQL التي إما تنجح جميعها أو تفشل جميعها. وبالتالي، في معاملة تتضمن N أوامر SQL، إذا فشلت الأمر I+1، فسيتم إلغاء الأوامر I السابقة. بالنسبة لعملية القراءة، لا تكون المعاملة ضرورية. ومع ذلك، فإن إنشاء معاملة للقراءة فقط قد يسمح لبعض أوامر SGBD بإجراء بعض التحسينات؛
- السطر 12: استخدام أمر [PreparedStatement]. عادةً ما تحتوي أوامر [PreparedStatement] على معلمات يُشار إليها بالحرف «؟». أما هنا فلا توجد معلمات. [PreparedStatement] هو أمر مُعد مسبقًا بواسطة SGBD. هذا الإعداد له تكلفة ولا يتم إجراؤه إلا مرة واحدة. بعد ذلك، يتم تنفيذ هذا الأمر المُعد مسبقًا بواسطة SGBD باستخدام معلمات فعلية مختلفة ستحل محل المعلمات الشكلية «؟». تجدر الإشارة إلى أنه من الأفضل تسمية الأعمدة المطلوبة بدلاً من استخدام الرمز * للحصول على جميع الأعمدة. ومن خلال تحديد أسماء الأعمدة، يمكن بعد ذلك الحصول على قيمها بناءً على مواقعها في الاستعلام SELECT؛
- السطر 13: تنفيذ [PreparedStatement]. يتم استرداد كائن من النوع [ResultSet]؛
يمثل الكائن من النوع [ResultSet] جدولًا، أي مجموعة من الصفوف والأعمدة. في أي لحظة معينة، لا يمكن الوصول إلا إلى صف واحد من الجدول يُسمى «الصف الحالي». عند الإنشاء الأولي لـ [ResultSet]، لا يوجد صف حالي. يلزم إجراء عملية [ResultSet.next()] للحصول عليه. توقيع الأسلوب next هو كما يلي:
تحاول هذه الطريقة الانتقال إلى السطر التالي من [ResultSet] وتُرجع true في حالة نجاحها، وfalse في حالة الفشل. في حالة النجاح، يصبح السطر التالي هو السطر الحالي الجديد. ويُفقد السطر السابق ولا يمكن الرجوع للخلف لاستعادته.
تحتوي جدول [ResultSet] على أعمدة تحمل أسماء labelCol1، labelCol2،... المحددة في الاستعلام [SELECT] الذي تم تنفيذه. باستخدام الاستعلام:
SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
- سيتم نقل العمود [ID] إلى عمود في الجدول [ResultSet] يُسمى [myId]؛
- سيتم نقل العمود [NOM] إلى عمود في [ResultSet] يُسمى [myNom]؛
- ...
في المثال أعلاه، تُسمى المعرّفات [myCol] «تسميات الأعمدة». وفي حالة عدم وجود هذه التسميات، فإن أسماء أعمدة الجدول [ResultSet] تعتمد على الجدول SGBD. عندما يعمل [SELECT] على جدول واحد، تكون تسميات الأعمدة افتراضيًا هي أسماء الأعمدة المطلوبة بواسطة SELECT. تنشأ المشكلة عندما يعمل [SELECT] على عدة جداول وتوجد فيها أسماء أعمدة متطابقة كما في المثال التالي:
SELECT PRODUITS.NOM, CATEGORIES.NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID
بافتراض أن الجدول [PRODUITS] يحتوي على مفتاح خارجي إلى الجدول [CATEGORIES]، والذي يرمز إليه بالعلاقة [Produits].CATEGORIE_ID --> [CATEGORIES].ID، وأن الجدولين [PRODUITS] و [CATEGORIES] يحتويان كلاهما على حقل [NOM]. في هذه الحالة، تعتمد الأسماء المحددة في الجدول [ResultSet] للأعمدة [PRODUITS.NOM] و [CATEGORIES.NOM] على الجدول SGBD. لضمان قابلية النقل بين ملف SGBD، يجب استخدام تسميات الأعمدة هنا، وسنكتب:
SELECT PRODUITS.NOM as p_NOM, CATEGORIES.NOM as c_NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID
لاستخدام الحقول المختلفة في السطر الحالي من [ResultSet]، تتوفر الطرق التالية:
للحصول على العمود المسمى «labelColi» من السطر الحالي، وبالتالي العمود في [SELECT] الذي يحمل هذا الاسم. ويشير Type إلى نوع الحقل coli. يمكن استخدام الطرق التالية لـ [getType]: getInt، getLong، getString، getDouble، getFloat، getDate، ... بدلاً من استخدام اسم العمود، يمكن استخدام موضعه في الاستعلام [SELECT] الذي تم تنفيذه:
حيث i هو مؤشر العمود المطلوب (i>=1).
- الأسطر 15-17: استرداد القيم التي تمت قراءتها في BD؛
- السطر 19: يتم التحقق من صحة المعاملة (ويُقال أيضًا «التثبيت»). وهذا ينهيها ويحرر الموارد التي كانت المعاملة SGBD قد استخدمتها من أجلها؛
- السطر 25: يتم تحرير الموارد في [finally]. ويستدعي هذا الأخير الطريقة التالية [doFinally]:
private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
// إغلاق ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
// إغلاق [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
if (connexion != null) {
try {
// إنهاء الاتصال
connexion.close();
} catch (SQLException e3) {
// معالجة الاستثناء
}
}
}
- الأسطر 3-9: إغلاق [ResultSet]؛
- الأسطر 11-17: إغلاق [PreparedStatement]؛
- الأسطر 18-27: إغلاق الاتصال؛
تبدو عمليات الإغلاق في الأسطر 3-17 زائدة عن الحاجة، حيث يتم إغلاق الاتصال في الأسطر 18-25. في الواقع، في بعض الحالات لا تكون زائدة عن الحاجة، ويُنصح بتركها [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].
- السطر 22: تتم معالجة الاستثناء بواسطة الطريقة التالية: [doCatchException]:
private static void doCatchException(Connection connexion, Throwable th) {
// إلغاء المعاملة
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
// معالجة الاستثناء
}
}
- الأسطر 4-6: يتم إلغاء المعاملة. وهذا ينهيها، وستتمكن SGBD من تحرير الموارد التي تم تخصيصها لها؛
3.2.4. الخطوة 3 - إصدار الأوامر SQL و [INSERT, UPDATE, DELETE]
الأوامر SQL و [INSERT, UPDATE, DELETE] هي عمليات تحديث: فهي تُعدّل قاعدة البيانات ولكنها لا تُرجع أي سطر. والمعلومة الوحيدة التي يتم إرجاعها هي عدد الأسطر التي تأثرت بعملية التحديث.
الرمز
Connection connexion = null;
PreparedStatement ps = null;
try {
// فتح الاتصال
connexion = DriverManager.getConnection(url, user, passwd);
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة/الكتابة
connexion.setReadOnly(false);
// تحديث الجدول
ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
// الفئة 1
ps.setInt(1, 10);
// التنفيذ
int nbLignes=ps.executeUpdate();
// تثبيت المعاملة
connexion.commit();
} catch (SQLException e1) {
// معالجة الاستثناء
doCatchException(connexion, e1);
} finally {
// معالجة جملة finally
doFinally(null, ps, connexion);
}
}
- السطر 9: يتم استخدام الاتصال للقراءة والكتابة؛
- السطر 11: عملية [PreparedStatement] مع معلمة واحدة (يُرمز لها بـ «؟»). يمكن أن يكون هناك عدة معلمات. يتم ترقيمها بدءًا من 1؛
- السطر 13: يتم تعيين قيمتها للمعلمة الوحيدة. المعلمة الأولى لـ [setType] هي موضع المعلمة في [PreparedStatement] (1، 2، ...) والثانية هي القيمة المخصصة لها. يمكن استخدام الطرق [setInt, setLong, setFloat, setDouble, setString, setDate, ...]؛
- السطر 15: تُستخدم الطريقة [executeUpdate] وليس [executeQuery] المخصصة للأوامر SELECT. تُرجع الطريقة عدد الأسطر المتأثرة بالعملية. قد تكون القيمة 0.
- السطر 17: تم التحقق من صحة المعاملة؛
3.2.5. الخطوة 4 - إغلاق الاتصال
يجب إغلاق الاتصال في أسرع وقت ممكن في بيئة متعددة المستخدمين لأن SGBD لا يقبل سوى عدد محدود من الاتصالات المفتوحة. في الأمثلة السابقة، تم إغلاقه في جملة [finally] الخاصة بعمليات SQL بحيث يتم إغلاقه سواء حدث استثناء أم لا.
3.3. تكوين الطبقة JDBC الخاصة بـ SGBD MySQL5
سنقوم بدراسة المشروع [mysql-config-jdbc] الذي يقوم بتكوين الطبقة JDBC أدناه:
![]() |
3.3.1. مشروع Eclipse
![]() |
3.3.2. تكوين Maven
ملف [pom.xml] الخاص بالمشروع هو كما يلي:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>configuration generic jdbc</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- التبعيات المتغيرة ********************************************** -->
<!-- برنامج التشغيل JDBC الخاص بـ SGBD -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- التبعيات الثابتة ********************************************** -->
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- مكتبة jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<!-- اختبار Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- السجلات -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
تم تجميع عدد من الأرشيفات في تكوين Maven هذا، وهي ضرورية إما لمشروع [mysql-config-jdbc] أو للمشاريع التي ستعتمد عليه:
- الأسطر 4-6: أرتيفاكت مافن الذي تم إنشاؤه بواسطة المشروع. وكما سبق ذكره، فإن جميع المشاريع من النوع [*-config-jdbc] تُنشئ هذا الأرتيفاكت نفسه. لذا يجب ألا يتم تحميل مشروعين من النوع [*-config-jdbc] في نفس الوقت؛
- الأسطر 9-13: مشروع Maven الأصلي لهذا المشروع. وهو يحدد إصدارات عدد كبير من الأرشيفات المستخدمة في نظام Spring. وهذا يغني عن تحديدها في المشاريع المشتقة منه؛
- الأسطر 18-21: أرشيف المشروع التجريبي JDBC الخاص بـ SGBD و MySQL5. وهو الأرشيف الوحيد المطلوب لمشروع [spring-jdbc-01]؛
- الأسطر 24-27: توفر الأداة [tomcat-jdbc] أرشيفًا ضروريًا لمشروعي JDBC و[spring-jdbc-02 à 04]؛
- الأسطر 29-36: توفر المكتبات اللازمة لإدارة jSON. تُستخدم في جميع مشاريع الوثيقة تقريبًا؛
- الأسطر 38-42: Google Guava هي مكتبة لإدارة المجموعات. تُستخدم في جميع مشاريع الوثيقة تقريبًا؛
- الأسطر 43-52: المكتبات التي تتيح كتابة الاختبارات التي تدمج Spring و JUnit. تُستخدم في جميع مشاريع الوثيقة تقريبًا؛
- الأسطر 54-57: مكتبات السجلات. تُستخدم في جميع مشاريع الوثيقة تقريبًا؛
- الأسطر 67-71: المكون الإضافي الذي يسمح بتثبيت عنصر المشروع [mysql-config-jdbc] في مستودع Maven المحلي؛
3.3.3. فئة التكوين [ConfigJdbc]
![]() |
فئة [ConfigJdbc] هي كما يلي:
package generic.jdbc.config;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
public class ConfigJdbc {
// معلمات الاتصال
public final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "root";
...
// الأوامر SQL [jdbc-01, jdbc-02]
public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
//public final static String V1_DELETE_PRODUITS = String.format("DELETE FROM %s", TAB_PRODUITS);
public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
// الأوامر SQL [jdbc-03]
public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
public final static String V2_DELETE_ALLPRODUITS = "DELETE FROM PRODUITS";
public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";
public final static String V2_SELECT_ALLPRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";
public final static String V2_SELECT_PRODUIT_BYNAME = "SELECT ID, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE NOM=?";
public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
...
}
تُستخدم فئة [ConfigJdbc] لتكوين الطبقة JDBC للمشاريع الأربعة [spring-jdbc-01 à 04]. يتعلق الجزء الأكبر من التكوين بمشروع [spring-jdbc-04]. سنعرض هذا الجزء عند دراسة هذا المشروع. لم نحتفظ أعلاه سوى بتكوين المشاريع [spring-jdbc-01 à 03].
- الأسطر 14-17: معلمات الاتصال بقاعدة البيانات MySQL5 و[dbproduits]؛
- الأسطر 20-25: الأوامر SQL المستخدمة في المشاريع [spring-jdbc-01 et 02]؛
- الأسطر 28-34: الأوامر SQL المستخدمة في المشروع [spring-jdbc-03]؛
تستفيد هذه الأوامر SQL من الجدول [PRODUITS] في قاعدة البيانات MySQL5 [dbproduits] التي تتكون هيكلها على النحو التالي:
![]() |
- [ID]: المفتاح الأساسي في الوضع AUTO_INCREMENT (إذا لم يتم تحديد مفتاح أساسي، فإن SGBD يقوم بإنشائه)؛
- [NOM]: اسم المنتج - فريد؛
- [CATEGORIE]: رقم فئته؛
- [PRIX]: سعره؛
- [DESCRIPTION]: وصف المنتج؛
3.3.4. الفئة [Produit]
![]() |
الفئة [Produit] هي صورة لسطر من الجدول [PRODUITS]:
package generic.jdbc.entities.dbproduits;
public class Produit {
// الحقول
private int id;
private String nom;
private int categorie;
private double prix;
private String description;
// المنشئات
public Produit() {
}
public Produit(int id, String nom, int categorie, double prix, String description) {
this.id = id;
this.nom = nom;
this.categorie = categorie;
this.prix = prix;
this.description = description;
}
// أدوات الاسترجاع والتعيين
...
}
سنحتاج لاحقًا إلى مقارنة منتجين لمعرفة ما إذا كانا متطابقين أم لا. سنقول إن المنتجين متساويين إذا كانت جميع حقولهما متساوية. ولهذا الغرض، سنقوم بإعادة تعريف الطريقة [equals] الخاصة بالفئة [Object] التي تنحدر منها الفئة [Produit]:
// طريقة المساواة
@Override
public boolean equals(Object o) {
// الحالات البسيطة
if (o == null || o.getClass() != this.getClass()) {
return false;
}
Produit p = (Produit) o;
return this == o
|| (this.id == p.id && this.nom.equals(p.getNom()) && this.categorie == p.categorie
&& Math.abs(this.prix - p.prix) < 1e-6 && this.description.equals(p.description));
}
- السطر 3: تتلقى الطريقة [equals] كائنًا o يجب عليها مقارنته بالكائن this؛
- الأسطر 5-7: الحالات البسيطة التي يمكن فيها القول على الفور إن الكائنين غير متساويين. توفر الدالة [Object].getClass() مثيلًا من النوع [Class]، وهو نوع يمثل الفئة الفعلية للكائن؛
- السطر 8: يتم تحويل الكائن o إلى منتج p؛
- السطر 9: إذا كانت المرجعان o و p على منتج ما متساويين، فإنهما يمثلان فعليًّا نفس المنتج؛
- السطر 9: إذا كان o و p مرجعين مختلفين لمنتجين لهما نفس الحقول، فسنقول إنهما متساويان. ونظرًا لأن السعر من النوع [double] ولأنه لا يوجد تمثيل دقيق للأعداد الحقيقية في علم الحاسوب، فسنعتبر أن السعرين متطابقان إذا كان الفرق بينهما لا يتجاوز 10-6؛
بالإضافة إلى ذلك، سنعيد تعريف الطريقة [hasCode] للفئة [Object]:
// رمز التجزئة
@Override
public int hashCode() {
return id + 2 * nom.hashCode() + 3 * categorie + 4 * description.hashCode();
}
يجب أن تكون قيم hashCode لمنتجين متطابقة إذا أعلنت الطريقة [equals] أن هذين المنتجين متساويان. تُستخدم قيمة hashCode هذه لتوزيع الكائنات في مجموعات مثل القواميس. فيما سبق، إذا كان المنتجان متطابقين، فسيكون لهما نفس قيمة hashCode.
3.3.5. الاستثناء [UncheckedException]
![]() |
لنأخذ البنية التالية بعين الاعتبار:
![]() |
- تُطلق الطبقة [JDBC] استثناءات من النوع [SQLException]. يجب أن ينتقل هذا الاستثناء عبر الطبقات حتى يصل إلى الطبقة الأعلى، وهي في هذه الحالة طبقة الاختبارات؛
يمكن للطبقة [DAO] أن تكتفي بالسماح لـ [SQLException] بالانتقال إلى طبقة الاختبارات. ولكن بما أن هذا الاستثناء غير خاضع للرقابة (ينشأ مباشرةً من [Exception])، فإن ذلك يعني أن واجهة [IDao] للطبقة [DAO] ستكون كما يلي:
public interface IDao {
// إضافة منتجات
public List<Produit> addProduits(List<Produit> produits) throws SQLException;
// قائمة بجميع المنتجات
public List<Produit> getAllProduits() throws SQLException;
// منتج معين
public Produit getProduitById(int id) throws SQLException;
public Produit getProduitByName(String name) throws SQLException;
// تحديث عدة منتجات
public int updateProduits(List<Produit> produits) throws SQLException;
// حذف جميع المنتجات
public int deleteAllProduits() throws SQLException;
// حذف عدة منتجات
public int deleteProduits(int[] ids) throws SQLException;
}
وهذا أمر مزعج للغاية لأنه يمنعنا من تنفيذ واجهة [IDao] بواسطة فئة تطلق استثناءً مختلفًا. لتجاوز هذه الصعوبة، ستقوم الطبقة [DAO] بإطلاق استثناء [DaoException] غير خاضع للتحكم (مشتق من [RuntimeException])، مما يتيح لنا تجنب استخدام الشرط [throws] في توقيع طرق الواجهة. وبالتالي، يمكن تنفيذ هذه الواجهة بواسطة أي فئة تطلق هي الأخرى استثناءً غير خاضع للرقابة قد يختلف عن الاستثناء [DaoException]. تصبح بنية نظامنا الآن كما يلي:
![]() |
لتسهيل إنشاء استثناءات غير خاضعة للرقابة لمختلف طبقات التطبيق، نقوم بإنشاء فئة أم لها باسم [UncheckedException]:
![]() |
package generic.jdbc.infrastructure;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
// فئة استثناء عامة
// الاستثناء غير متحكم فيه
public class UncheckedException extends RuntimeException {
// تم إنشاء الرقم التسلسلي ID
private static final long serialVersionUID = -2924871763340170310L;
// الخصائص
private int code;
private String trace;
private List<ShortException> exceptions;
// منشئات
public UncheckedException() {
super();
}
public UncheckedException(int code, Throwable e, String simpleClassName) {
super(e);
// محلي
this.code = code;
this.exceptions = getErreursForException(e);
// تتبع
String fileName = String.format("%s.java", simpleClassName);
StackTraceElement[] traces = e.getStackTrace();
boolean trouve = false;
for (int i = 0; !trouve && i < traces.length; i++) {
StackTraceElement trace = traces[i];
if (fileName.equals(trace.getFileName())) {
this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
trouve = true;
}
}
}
@Override
public String getMessage() {
return this.toString();
}
@Override
public void printStackTrace() {
System.out.println(this);
}
// قائمة رسائل الخطأ الخاصة باستثناء
private List<ShortException> getErreursForException(Throwable th) {
// استرداد عناصر مكدس الاستثناء
Throwable cause = th;
List<ShortException> exceptions = new ArrayList<ShortException>();
while (cause != null) {
// استرداد الاستثناء الحالي
exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
// الاستثناء التالي
cause = cause.getCause();
}
return exceptions;
}
@Override
public String toString() {
ObjectMapper jsonMapper = new ObjectMapper();
try {
return String.format("[code=%s, trace=%s, exceptions=%s", code, trace, jsonMapper.writeValueAsString(exceptions));
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// دالات الاسترجاع والتعيين
...
}
- السطر 12: الفئة مشتقة من [RuntimeException]، وبالتالي فهي نوع من الاستثناءات غير المراقبة. وستُستخدم لتغليف استثناء مراقب (SQLException) في نوع من الاستثناءات غير المراقبة (UncheckedException)؛
- ولتمييز الاستثناءات من النوع [UncheckedException] عن بعضها البعض، يمكن تعيين رمز لها يتم تخزينه في الحقل الخاص في السطر 18. سيتمكن كود Java الذي يعترض استثناءً من النوع [UncheckedException] من الوصول إلى رمز الخطأ هذا بفضل الطريقة [getCode] (السطر 80 وما بعده)؛
- السطر 20: يخزن رسائل الأخطاء من مكدس الاستثناء المُغلف؛
- الأسطر 23-43: الطرق المختلفة لإنشاء كائن من النوع [UncheckedException]؛
- الأسطر 56-67: طريقة خاصة تسمح بإنشاء قائمة الأخطاء الواردة في السطر 20 انطلاقًا من كائن من نوع [Throwable] أو مشتق منه، ولا سيما النوع [Exception]؛
- الأسطر 69-78: تُرجع الطريقة [toString] سلسلة أحرف تمثل الاستثناء. لعرض قائمة الأخطاء في السطر 20، تستخدم هذه الطريقة مكتبة jSON. وتوجد هذه المكتبة ضمن تبعيات Maven للمشروع:
<!-- مكتبة jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
- الأسطر 45-48: تعيد تعريف الطريقة [getMessage] للفئة الأم [RuntimeException]. وتُرجع هذه الطريقة هنا التوقيع [toString] للفئة؛
- الأسطر 50-53: تعيد تعريف الطريقة [printStackTrace] للفئة الأم [RuntimeException]. وسيتم عرض التوقيع [toString] للفئة؛
تسجل الفئة [UncheckedException] في حقل السطر 20 قائمة بالاستثناءات الموصوفة بالنوع [ShortException] التالي:
package pam.dao.exceptions;
public class ShortException {
// الخصائص
private String className;
private String errorMessage;
// منشئات
public ShortException() {
}
public ShortException(String className, String errorMessage) {
this.className = className;
this.errorMessage = errorMessage;
}
// دالات الحصول والتعيين
...
}
- السطر 6: اسم فئة الاستثناء الذي حدث؛
- السطر 7: رسالة الخطأ المرتبطة؛
دعونا نلقي نظرة على المنشئ التالي لفئة [UncheckedException]:
public UncheckedException(int code, Throwable e, String simpleClassName) {
super(e);
// محلي
this.code = code;
this.exceptions = getErreursForException(e);
// التتبع
String fileName = String.format("%s.java", simpleClassName);
StackTraceElement[] traces = e.getStackTrace();
boolean trouve = false;
for (int i = 0; !trouve && i < traces.length; i++) {
StackTraceElement trace = traces[i];
if (fileName.equals(trace.getFileName())) {
this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
trouve = true;
}
}
}
- السطر 1، المعلمات هي كما يلي:
- [code]: رمز الخطأ؛
- [e]: الاستثناء الذي يتم تغليفه. [Throwable] هي الفئة الأم للفئة [Exception] وتشتق مباشرةً من الفئة [Object]. وهي الفئة الأم لجميع فئات C التي يمكن من خلالها كتابة [throw c;] حيث c هي مثيل لـ C؛
- [simpleClassName]: الاسم البسيط لفئة كود المستخدم حيث تم اكتشاف الاستثناء e؛
- السطر 4: يتم تسجيل رمز الخطأ؛
- السطر 5: يتم إنشاء قائمة [ShortException] استنادًا إلى [Throwable e] الذي تم تمريره كمعلمة؛
- الأسطر 7-16: ندرس ما يُعرف بـ«آثار الاستثناء». يحدث الاستثناء الأولي في موضع محدد من الكود، ثم يتتبع مساره إلى الدالة التي استدعت الدالة التي وقع فيها الاستثناء، وهكذا دواليك حتى يوقفه تعبير try/catch. خلال عملية التتبع هذه، تترك الاستثناء الأولي آثارًا مخزنة في المصفوفة [e.stackTrace] الخاصة بالاستثناء e. يتم الحصول على هذه الآثار هنا في السطر 8، انطلاقًا من المصفوفة [Throwable e] التي تم تمريرها كمعلمة. كل عنصر من النوع [StackTraceElement] هو كائن يحتوي بين حقوله على ما يلي:
- [fileName]: اسم ملف Java الذي حدثت فيه الاستثناء؛
- [lineNumber]: رقم السطر في هذا الملف الذي حدثت فيه الاستثناء؛
- [methodName]: اسم الأسلوب في هذا الملف الذي حدثت فيه الاستثناء؛
- تبحث الأسطر 10-16 في مصفوفة سجلات الاستثناء الذي تم تمريره كمعلمة، عن أول ظهور للشرط [trace.fileName==simpleClassName.java] حيث [simpleClassName] هو المعلمة الثالثة لمُنشئ الكائن. الفكرة هي تذكر المكان الذي حدثت فيه الاستثناء في كود المستخدم. وسيقوم هذا الكود بتغليف الاستثناء بالطريقة التالية:
- السطر 13: يتم إنشاء سلسلة أحرف من النوع [fileName, methodName, lineNumber] تحدد المكان في كود المستخدم الذي تم فيه إيقاف الاستثناء؛
الآن، دعونا نلقي نظرة على الكود الذي يسجل قائمة الاستثناءات من مكدس الاستثناءات الخاص بالاستثناء [Throwable th] الذي تم تغليفه بواسطة المنشئ السابق:
// قائمة رسائل الخطأ الخاصة باستثناء
private List<ShortException> getErreursForException(Throwable th) {
// استرداد عناصر مكدس الاستثناء
Throwable cause = th;
List<ShortException> exceptions = new ArrayList<ShortException>();
while (cause != null) {
// استرداد الاستثناء الحالي
exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
// الاستثناء التالي
cause = cause.getCause();
}
return exceptions;
}
أثناء صعودها نحو الطريقة التي أوقفتها باستخدام try / catch، يمكن أن يتم تغليف الاستثناء الأولي e في استثناء آخر. وعندئذٍ يكون هذا الاستثناء الأخير هو الذي يصعد نحو الطريقة التي ستوقفه نهائيًا. وبالتالي، يمكن أن يخضع هو أيضًا لعملية التغليف. في النهاية، عندما تقرر إحدى الطرق إيقاف استثناء th واستغلاله، ستجد الاستثناء الأولي e مدفونًا في قاع مكدس الاستثناءات. وهكذا، في المثال أعلاه، فإن المعلمة [Throwable th] ليست سوى الجزء المرئي من جبل جليدي من الاستثناءات. ويسمح سمة [th.cause] بمعرفة الاستثناء الذي تقوم هي نفسها بتغليفه. وهكذا دواليك. وعندما يكون الاستثناء e على النحو التالي: [e.getCause()==null]، فهذا يعني أن e هو الاستثناء الأولي.
- السطر 8: لكل استثناء في مكدس الاستثناءات الخاص بـ [Throwable th]، يتم تخزين معلومتين:
- [getClass().getName()]: الاسم الكامل للاستثناء؛
- [getMessage()]: رسالة الخطأ المرتبطة بها؛
3.4. مثال-01
3.4.1. بنية المشروع
![]() |
في هذا المثال، يستخدم برنامج وحدة التحكم واجهة الطبقة [JDBC].
3.4.2. مشروع Eclipse
نقوم بإنشاء مشروع Spring / Maven [spring-jdbc-01] باتباع الخطوات الموضحة في الفقرة 2.5.2.1.
![]() |
المشروع هو مشروع Maven محدد بواسطة الملف [pom.xml] التالي:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jdbc-generic-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jdbc-generic-01</name>
<description>Demo project for API JDBC</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
<relativePath /> <!-- البحث عن العنصر الأصلي من المستودع -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- تكوين JDBC لـ SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- الأسطر 28-32: يستخدم المشروع الأرتيفاكت [generic-config-jdbc] من مشروع [mysql-config-jdbc] الذي درسناه للتو. وبالتالي، فإن المشروع [spring-jdbc-01] يمكنه الوصول إلى جميع عناصر المشروع [mysql-config-jdbc]؛
يمكن ملاحظة هذه النقطة الأخيرة بطريقتين من خلال فحص تبعيات Maven للمشروع:
![]() |
- في [2]، نلاحظ أن المشروع [mysql-config-jdbc] موجود ضمن تبعيات Maven للمشروع. ونظرًا لأن هذه التبعيات موجودة في مسار الفئات (Classpath) للمشروع، فهذا يعني أن المشروع [mysql-config-jdbc] موجود أيضًا في مسار الفئات هذا، وبالتالي فإن فئاته وواجهاته مرئية في المشروع [spring-jdbc-01]؛
لا يحتاج مشروع Maven [mysql-config-jdbc] إلى أن يكون موجودًا في علامة التبويب [Package Explorer] ليكون قابلاً للاستخدام من قبل مشاريع Maven الأخرى. يكفي أن يكون موجودًا في مستودع Maven المحلي. على عكس مشروع IDE مثل NetBeans، فإن هذا التواجد ليس تلقائيًا مع Eclipse. لذا يجب فرضه:
![]() |
لقد تناولنا الشروط التي تتيح هذا الإنشاء في الفقرة 2.3.5. وبمجرد إتمام العملية، يمكن عندئذٍ إزالة المشروع [mysql-config-jdbc] من علامة التبويب [Package Explorer]:
![]() |
- يجب عدم تحديد [3] الذي يحذف المشروع فعليًّا من القرص، مما يجعله غير قابل للاستعادة؛
تؤدي هذه العملية إلى إعادة حساب تبعيات Maven للمشاريع التي تعتمد على المشروع الذي تم إخراجه من [Package Explorer]. وهذا يؤدي إلى تغيير الفرع [Maven Dependencies] لهذه المشاريع. على سبيل المثال، بالنسبة للمشروع [spring-jdbc-01]، يصبح الفرع [Maven Dependencies] كما يلي:
![]() |
هذه المرة، لم تعد التبعية مرتبطة بمشروع ما، بل بأداة Maven الخاصة به، وهي في هذه الحالة الأداة [generic-config-jdbc] [1]. نلاحظ أننا نتمتع بالوصول الكامل إلى جميع الفئات والواجهات الخاصة بهذا الأرتيفاكت. وكما ذكرنا سابقًا، سيتم إنشاء هذا الأرتيفاكت بواسطة جميع المشاريع [*-config-jdbc]. لتجنب الأخطاء، سنقوم بما يلي:
- نحتفظ دائمًا بمشروع واحد فقط هو [*-config-jdbc] في علامة التبويب [Package Explorer]؛
- سنقوم بتحديث تكوين Maven لجميع المشاريع الموجودة في علامة التبويب [Package Explorer] (Alt-F5) بحيث تظهر في تبعيات Maven الخاصة بها المشروع [*-config-jdbc] المستخدم؛
3.4.3. الهيكل الأساسي للفئة الرئيسية
![]() |
الهيكل الأساسي للفئة الرئيسية [IntroJdbc01] هو كما يلي:
package spring.jdbc;
import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class IntroJdbc01 {
// الثوابت
final static ObjectMapper jsonMapper = new ObjectMapper();
public static void main(String[] args) {
// تحميل برنامج التشغيل JDBC الخاص بـ SGBD
try {
Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
} catch (ClassNotFoundException e1) {
doCatchException("Pilote JDBC introuvable", null, e1);
return;
}
// إفراغ الجدول [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
// يتم ملؤها
System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
insert();
// قراءتها
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// تحديث
System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
update();
// عرض
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// إفراغ الجدول [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
// عرضها
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// INSERTion من عنصرين متطابقين
// يجب أن تفشل العملية INSERTion ولا يتم إدراج أي من العنصرين بسبب المعاملة
System.out.println(String.format("------------------------------ %s",
"Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
insert2();
// نقوم بالتحقق
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// انتهى
System.out.println(String.format("------------------------------ %s", "Travail terminé"));
}
// قائمة المنتجات
private static void select() {
...
}
// عرض كائن jSON
private static void affiche(Object object) {
...
}
// حذف المنتجات
public static void delete() {
...
}
// إضافة منتجات
public static void insert() {
...
}
// إضافة منتجين لهما نفس المفاتيح الأساسية
public static void insert2() {
...
}
// تحديث بعض المنتجات
public static void update() {
...
}
private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
// إغلاق ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
// إغلاق [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
if (connexion != null) {
try {
// إنهاء الاتصال
connexion.close();
} catch (SQLException e3) {
// يتم عرض رسائل الخطأ
show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
getErreursFromThrowable(e3));
}
}
}
private static void doCatchException(String title, Connection connexion, Throwable th) {
// يتم عرض رسائل الخطأ
show(title, getErreursFromThrowable(th));
// إلغاء المعاملة
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
// عرض رسائل الخطأ
show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
}
}
private static List<String> getErreursFromThrowable(Throwable th) {
// استرداد قائمة رسائل الخطأ الخاصة بالاستثناء
List<String> erreurs = new ArrayList<String>();
while (th != null) {
// رسالة خطأ الكائن القابل للرمي
erreurs.add(th.getMessage());
// الانتقال إلى سبب الاستثناء
th = th.getCause();
}
// النتيجة
return erreurs;
}
private static void show(String title, List<String> messages) {
// العنوان
System.out.println(String.format("%s : ", title));
// الرسائل
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
- الأسطر 23-29: تحميل برنامج التشغيل JDBC من SGBD. في السطر 25، يتم استخدام الثابت [ConfigJdbc.DRIVER_CLASSNAME] المُعرَّف في المشروع [mysql-config-jdbc]؛
- الأسطر 136-147: تُرجع الطريقة [getErreursFromThrowable] قائمة رسائل الخطأ المُغلفة في كائن من النوع [Throwable]، وهو الفئة الأم للفئة [Exception]. قد تحتوي إحدى الاستثناءات على استثناء آخر يمكن الحصول عليه باستخدام الطريقة [Throwable].getCause(). وبذلك يتم استعراض جميع الاستثناءات المُغلفة في الكائن [Throwable]؛
- الأسطر 149-156: تعرض الطريقة [show(String title, List<String> messages)] الرسائل مسبوقة بالنص [title]؛
- الأسطر 122-134: تعالج الطريقة [doCatchException(String title, Connection connexion, Throwable th))] الاستثناءات التي تواجهها طرق الفئة. ويمثل المعامل [Throwable th] الاستثناء الذي يتم معالجته. والهدف من هذه الطريقة هو:
- إلغاء المعاملة الجارية للكائن [Connection connexion] (الأسطر 127-129)؛
- كتابة رسائل الخطأ المضمنة في الاستثناء [Throwable th] (السطور 124، 132)؛
- الأسطر 93-120: تتولى الطريقة [doFinally(ResultSet rs, PreparedStatement ps, Connection connexion)] إدارة الفرع [finally] من طرق الوصول إلى SGBD. وتهدف إلى تحرير الموارد التي استهلكها الاتصال؛
3.4.4. حذف محتوى جدول المنتجات
تقوم الطريقة [delete] بحذف محتوى الجدول:
// حذف المنتجات
public static void delete() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// فتح جلسة الاتصال
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة/الكتابة
connexion.setReadOnly(false);
// إفراغ الجدول [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
ps.executeUpdate();
// تثبيت المعاملة
connexion.commit();
} catch (SQLException e1) {
// معالجة الاستثناء
doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
} finally {
// معالجة جملة finally
doFinally(null, ps, connexion);
}
}
يستخدم السطر 7 الثوابت التالية من الفئة [ConfigJdbc]:
public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "";
في السطر 13، يكون الأمر SQL المُعدّ كما يلي:
public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
تستخدم الطريقة [delete] المعاملات. تتيح المعاملة تجميع أوامر SQL التي يجب أن تنجح جميعها أو يتم إلغاؤها جميعًا. هناك أربع عمليات يجب معرفتها:
- بدء المعاملة: [connexion.setAutoCommit(false)]؛
- إنهاء المعاملة بنجاح: [connexion.commit()]. في هذه الحالة، يتم اعتماد جميع العمليات التي تم إجراؤها على BD خلال المعاملة؛
- نهاية المعاملة بفشل: [connexion.rollback()]. في هذه الحالة، يتم إلغاء جميع العمليات التي تمت على BD خلال المعاملة؛
في أمثلةنا، كلما حدث استثناء، نقوم بإلغاء المعاملة في الأسلوب [doCatchException]:
private static void doCatchException(String title, Connection connexion, Throwable th) {
// عرض رسائل الخطأ
Static.show(title, Static.getErreursFromThrowable(th));
// إلغاء المعاملة
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
// عرض رسائل الخطأ
Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
}
}
3.4.5. إنشاء محتوى جدول المنتجات
تقوم الطريقة [insert] بإنشاء محتوى الجدول:
public static void insert() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// فتح اتصال
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة/الكتابة
connexion.setReadOnly(false);
// ملء الجدول
ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
for (int i = 0; i < 10; i++) {
// التحضير
int n = i + 1;
ps.setInt(1, n);
ps.setString(2, String.format("NOM%s", n));
ps.setInt(3, n / 5 + 1);
ps.setDouble(4, 100 * (1 + (double) i / 100));
ps.setString(5, String.format("DESC%s", n));
// التنفيذ
ps.executeUpdate();
}
// تثبيت المعاملة
connexion.commit();
} catch (SQLException e1) {
// معالجة الاستثناء
doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
} finally {
// معالجة جملة finally
doFinally(null, ps, connexion);
}
}
السطر 12، الأمر SQL الذي تم إعداده هو كما يلي:
public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
3.4.6. عرض محتوى جدول المنتجات
تعرض الطريقة [select] محتوى الجدول:
// قائمة المنتجات
private static void select() {
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// فتح اتصال
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة فقط
connexion.setReadOnly(true);
// قراءة الجدول [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
rs = ps.executeQuery();
System.out.println("Liste des produits : ");
while (rs.next()) {
affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
// تثبيت المعاملة
connexion.commit();
} catch (SQLException e1) {
// معالجة الاستثناء
doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
} finally {
// معالجة جملة finally
doFinally(rs, ps, connexion);
}
}
السطر 14، الأمر SQL الذي تم إعداده هو كما يلي:
public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
الطريقة [affiche] (السطر 18) هي كما يلي:
// عرض كائن jSON
private static void affiche(Object object) {
try {
System.out.println(jsonMapper.writeValueAsString(object));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
وهي تعرض تمثيل jSON للكائن الذي تم تمريره كمعلمة (انظر jSON الفقرة 23.12).
3.4.7. تحديث محتوى الجدول
تقوم الطريقة [update] بتحديث بعض المنتجات:
// تحديث بعض المنتجات
public static void update() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// فتح جلسة اتصال
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة/الكتابة
connexion.setReadOnly(false);
// يتم تحديث الجدول
ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
// الفئة 1
ps.setInt(1, 1);
// التنفيذ
ps.executeUpdate();
// تثبيت المعاملة
connexion.commit();
} catch (SQLException e1) {
// معالجة الاستثناء
doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
} finally {
// معالجة جملة finally
doFinally(null, ps, connexion);
}
}
السطر 13، الأمر SQL الذي تم إعداده هو كما يلي:
public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
3.4.8. دور المعاملة
تقوم الطريقة [insert2] بإدراج منتجين لهما نفس المفتاح الأساسي في الجدول، وهو أمر غير ممكن. ونظرًا لأننا في معاملة، فسيتم إلغاء الإدراج الأول.
// إضافة منتجين لهما نفس المفاتيح الأولية
public static void insert2() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// فتح اتصال
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة/الكتابة
connexion.setReadOnly(false);
// إضافة سطر واحد
ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
// التنفيذ
ps.executeUpdate();
// إضافة نفس السطر للمرة الثانية، وبالتالي باستخدام نفس المفتاح الأساسي
// يجب أن تفشل المعاملة INSERTion ولا يجب إدراج أي من العنصرين بسبب المعاملة
ps.executeUpdate();
// تثبيت المعاملة
connexion.commit();
} catch (SQLException e1) {
// معالجة الاستثناء
doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
connexion, e1);
} finally {
// معالجة جملة finally
doFinally(null, ps, connexion);
}
}
السطر 13، الأمر SQL المُعد هو كما يلي:
public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
3.4.9. النتائج
يتم تنفيذ تكوين التنفيذ المسمى [spring-jdbc-generic-01.IntroJdbc01]:
![]() |
ونحصل على النتائج التالية في وحدة التحكم:
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire :
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
------------------------------ Travail terminé
- السطر 30: قبل إدراج المنتجين اللذين لهما نفس المفتاح الأساسي، تكون الجدولة فارغة؛
- السطر 35: بعد إدراج المنتجين اللذين لهما نفس المفتاح الأساسي، تكون الجدولة فارغة. وهذا يوضح دور المعاملة:
- نجح الإدراج الأول. ولا يوجد أي سبب لفشله؛
- يفشل الإدراج الثاني (السطر 32). وبالتالي، نظرًا لأن هذين الإدراجين يقعان ضمن نفس المعاملة، يتم إلغاء جميع أوامر SQL الخاصة بها، بما في ذلك الإدراج الأول.
3.4.10. الخلاصة
ما يلفت الانتباه في الأكواد السابقة هو الحيز الكبير الذي تشغله معالجة الاستثناء [SQLException]. ونظرًا لأن أي عملية JDBC قد تؤدي إلى إطلاق هذا الاستثناء، فإننا نجد العديد من عبارات try / catch في الكود.
3.5. المثال-02
سنستأنف التطبيق السابق باستخدام مصدر بيانات من النوع [javax.sql.DataSource]:

سنستخدم مصدر بيانات تم تنفيذه بواسطة الفئة [org.apache.tomcat.jdbc.pool.DataSource]. تستخدم هذه الفئة مجموعة اتصالات، أي مجموعة من الاتصالات المفتوحة:
- عند إنشاء مثيل للمجموعة، يتم فتح عدد معين من الاتصالات مع قاعدة البيانات. هذا العدد قابل للتكوين؛
- عندما يفتح كود Java اتصالاً، يتم توفيره من المجمع؛
- عندما يغلق كود Java اتصالاً، يتم إرجاعه إلى مجموعة الاتصالات؛
في النهاية، لا تُفتح الاتصالات إلا مرة واحدة، مما يحسّن أداء الوصول إلى قاعدة البيانات. سيتم تعريف مصدر البيانات في فئة تكوين Spring
3.5.1. بنية المشروع
![]() |
في هذا المثال، يستخدم برنامج وحدة التحكم واجهة الطبقة [JDBC].
3.5.2. مشروع Eclipse
يمكن الحصول على مشروع Eclipse الجديد عن طريق نسخ المشروع السابق [1-6]:
![]() |
![]() |
ثم يتم تحديث مشروع [6] إلى [7]:
3.5.3. تكوين Maven
المشروع [7] هو مشروع Maven محدد بواسطة الملف [pom.xml] التالي:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jdbc-generic-02</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jdbc-generic-02</name>
<description>Demo project for API JDBC</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
<relativePath /> <!-- البحث عن العنصر الأصلي من المستودع -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- تكوين JDBC لـ SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- الأسطر 28-33: تبعية Maven لمشروع [mysql-config-jdbc]؛
المشروع [mysql-config-jdbc] هو الذي يتضمن في تبعياته في مافن المكتبة التي توفر تنفيذًا لمصدر بيانات من النوع [javax.sql.DataSource] (انظر الفقرة 3.3.2):
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
3.5.4. تكوين Spring
![]() |
فئة تكوين Spring [AppConfig] هي كما يلي:
package spring.jdbc;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {
// مصدر البيانات
@Bean
public DataSource dataSource() {
// مصدر البيانات TomcatJdbc
DataSource dataSource = new DataSource();
// تكوين الوصول JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
// الاتصالات المفتوحة في البداية
dataSource.setInitialSize(5);
// النتيجة
return dataSource;
}
}
- السطر 10: [AppConfig] هي فئة تكوين Spring؛
- السطر 11: استيراد فئة التكوين [generic.jdbc.config.ConfigJdbc.class] المُعرَّفة في المشروع [mysql-config-jdbc]. وهذا يعني أن جميع الفاصوليا المُعرَّفة في ملف التكوين هذا متوفرة؛
- الأسطر 14-27: عنصر Spring الذي يحدد مصدر البيانات؛
- السطر 17: إنشاء مصدر البيانات الذي لم يتم تكوينه بعد؛
- الأسطر 19-22: المعلومات التي تسمح لمصدر البيانات بالاتصال بقاعدة البيانات؛
- السطر 24: إنشاء مجموعة من 5 اتصالات. لا نحتاج هنا سوى اتصال واحد فقط. لا توجد أبدًا عدة اتصالات متزامنة؛
3.5.5. الفئة الرئيسية
الفئة الرئيسية [IntroJdbc02] هي كما يلي:
package spring.jdbc;
import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class IntroJdbc02 {
// التعيين jSON
final static ObjectMapper jsonMapper = new ObjectMapper();
// مصدر البيانات
private static DataSource dataSource;
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = null;
try {
// استرداد سياق Spring
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
// استرداد مصدر البيانات
dataSource = ctx.getBean(DataSource.class);
// إفراغ الجدول [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
...
// انتهى
System.out.println(String.format("------------------------------ %s", "Travail terminé"));
}
// قائمة المنتجات
private static void select() {
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// فتح الاتصال
connexion = dataSource.getConnection();
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة فقط
connexion.setReadOnly(true);
// قراءة الجدول [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
rs = ps.executeQuery();
System.out.println("Liste des produits : ");
while (rs.next()) {
affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
// تثبيت المعاملة
connexion.commit();
} catch (SQLException e1) {
// معالجة الاستثناء
doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
} finally {
// معالجة جملة finally
doFinally(rs, ps, connexion);
}
}
...
- السطر 25: مصدر البيانات. تجدر الإشارة إلى أنه من النوع [javax.sql.DataSource] (السطر 13) وهو واجهة؛
- السطر 31: إنشاء مثيلات كائنات Spring؛
- السطر 32: الحصول على مرجع لمصدر البيانات. تجدر الإشارة إلى أنه لم يتم ذكر الفئة المستخدمة فعليًا في أي مرحلة. وبالتالي، لا يوجد هنا ما يشير إلى استخدام تنفيذ [TomcatJdbc]؛
- السطر 49: الحصول على اتصال مفتوح. وبهذه الطريقة، تحصل الطرق المختلفة لـ [IntroJdbc02] على اتصال بقاعدة البيانات. أما باقي الكود فهو مطابق لكود الفئة [IntroJdbc01]؛
3.5.6. الاختبارات
يتم تنفيذ تكوين التشغيل المسمى [spring-jdbc-generic-02.IntroJdbc02]:
![]() |
نحصل على نفس النتائج التي حصلنا عليها سابقًا (الفقرة 3.4.9).
3.6. المثال-03
3.6.1. بنية المشروع
![]() |
في هذا المثال، يتم عزل طرق الوصول إلى البيانات في طبقة [dao]. وسيتم اختبارها من خلال اختبار JUnit.
3.6.2. مشروع Eclipse
مشروع Eclipse [spring-jdbc-03] هو مشروع Spring / Maven تم إنشاؤه مثل المشروع السابق ثم تم استكماله على النحو التالي:
![]() | ![]() |
تؤدي الحزم المختلفة الأدوار التالية:
- [spring.jdbc.config]: تكوين مشروع Spring؛
- [spring.jdbc.dao]: تنفيذ الطبقة [DAO]؛
- [spring.jdbc.infrastructure]: تنفيذ الاستثناء غير المتحكم فيه [DaoException]؛
3.6.3. تكوين Maven
يتم تكوين مشروع Maven بواسطة الملف [pom.xml] التالي:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jdbc-generic-03</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jdbc-generic-03</name>
<description>Demo project for API JDBC</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
<relativePath /> <!-- البحث عن العنصر الأصلي من المستودع -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- تكوين JDBC لـ SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
وهو مطابق لتلك الخاصة بمشروع [spring-jdbc-02]. ويستخدم على وجه الخصوص تبعية مافن الخاصة بمشروع [mysql-config-jdbc] (الأسطر 28-32).
3.6.4. واجهة الطبقة [DAO]
![]() |
تقدم الطبقة [DAO] واجهة [IDao] التالية:
package spring.jdbc.dao;
import java.util.List;
import spring.jdbc.entities.Produit;
public interface IDao {
// إضافة منتجات
public List<Produit> addProduits(List<Produit> produits);
// قائمة بجميع المنتجات
public List<Produit> getAllProduits();
// منتج معين
public Produit getProduitById(int id);
public Produit getProduitByName(String name);
// تحديث عدة منتجات
public int updateProduits(List<Produit> produits);
// حذف جميع المنتجات
public int deleteAllProduits();
// حذف عدة منتجات
public int deleteProduits(int[] ids);
}
3.6.5. الفئة [DaoException]
تكتفي الفئة [DaoException] بتوسيع نطاق الفئة [UncheckedException] المعروضة في الفقرة 3.3.5:
![]() |
package spring.jdbc.infrastructure;
public class DaoException extends UncheckedException {
private static final long serialVersionUID = 1L;
// الشركات المصنعة
public DaoException() {
super();
}
public DaoException(int code, Throwable e, String className) {
super(code, e, className);
}
}
3.6.6. تكوين مشروع Spring
![]() |
الفئة [AppConfig] التي تهيئ مشروع Spring مطابقة لملف تهيئة Spring في المثال [spring-jdbc-02] باستثناء السطر 11:
package spring.jdbc.config;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
public class AppConfig {
// مصدر البيانات
@Bean
public DataSource dataSource() {
// مصدر البيانات TomcatJdbc
DataSource dataSource = new DataSource();
// تكوين الوصول JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
// الاتصالات المفتوحة في البداية
dataSource.setInitialSize(5);
// النتيجة
return dataSource;
}
}
- السطر 11: سيتم فحص الحزمة [spring.jdbc.dao] للبحث عن مكونات Spring أخرى غير تلك المحددة في ملف التكوين هذا؛
3.6.7. تنفيذ الطبقة [DAO]
![]() |
![]() |
تجدر الإشارة (الفقرة 3.6.4) إلى أن الطبقة [DAO] تنفذ الواجهة [IDao] التالية:
package spring.jdbc.dao;
import generic.jdbc.entities.dbproduits.Produit;
import java.util.List;
public interface IDao {
// إضافة منتجات
public List<Produit> addProduits(List<Produit> produits);
// قائمة بجميع المنتجات
public List<Produit> getAllProduits();
// منتج معين
public Produit getProduitById(int id);
public Produit getProduitByName(String name);
// تحديث عدة منتجات
public int updateProduits(List<Produit> produits);
// حذف جميع المنتجات
public int deleteAllProduits();
// حذف عدة منتجات
public int deleteProduits(int[] ids);
}
تقوم كلتا الفئتين [Dao1, Dao2] بتنفيذ هذه الواجهة. الفئة [Dao2] هي نسخة معدلة من الفئة [Dao1] تضيف ميزة نحوية جديدة. سنركز على الفئة [Dao1]. وهيكلها الأساسي هو كما يلي:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.jdbc.infrastructure.DaoException;
@Component("dao1")
public class Dao1 implements IDao {
// اسم الفئة
private String simpleClassName = getClass().getSimpleName();
// مصدر البيانات
@Autowired
protected DataSource dataSource;
// الشركة المصنعة
public Dao1() {
System.out.println("building Dao1...");
}
// ------------------------------- الواجهة
@Override
public List<Produit> getAllProduits() {
...
}
@Override
public Produit getProduitById(int id) {
...
}
@Override
public Produit getProduitByName(String name) {
...
}
@Override
public List<Produit> addProduits(List<Produit> produits) {
....
}
@Override
public int updateProduits(List<Produit> produits) {
...
}
@Override
public int deleteAllProduits() {
...
}
@Override
public int deleteProduits(int[] ids) {
...
}
// ---------------------------------------- الطرق المحلية
// معالجة finally
protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
DaoException daoException) {
...
}
// معالجة catch
protected DaoException doCatchException(Connection connexion, Throwable th, int code, DaoException daoException) {
...
}
- السطر 20: الفئة [Dao] هي مكون Spring يُسمى [dao1]. هذا الاسم اختياري. وعندما لا يكون موجودًا، يُستخدم اسم الفئة مع تحويل الحرف الأول الكبير إلى صغير؛
- السطر 24: اسم الفئة. يُفضل تجنب كتابة [Dao] بشكل ثابت لإتاحة إمكانية إعادة تسمية الفئة دون الحاجة إلى إعادة تعريف هذا الحقل الذي يظل بذلك صالحًا دائمًا؛
- السطران 26-27: إدراج مصدر البيانات [tomcat-jdbc] المُعرَّف في فئة التكوين [AppConfig]؛
- الأسطر 36-68: تنفيذ الواجهة [IDao]؛
- الأسطر 78-80: الإدارة المركزية لـ catch للطرق المختلفة؛
- الأسطر 72-75: الإدارة المركزية لـ finally للطرق المختلفة؛
تتم إدارة واجهة catch للطرق المختلفة على النحو التالي:
// معالجة catch
protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
// إلغاء المعاملة
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
e2.printStackTrace();
}
// daoException
return new DaoException(code, th, simpleClassName);
}
- السطر 2: يتم إعلان الطريقة [protected] مما يسمح للفئات الفرعية باستخدامها دون أن تكون عامة. وتتلقى المعلمات التالية:
- [Connection connexion]: الاتصال بـ SGBD - ربما null؛
- [Throwable th]: الاستثناء الذي حدث والذي سيتم تغليفه في نوع [DaoException]؛
- [int code]: رمز خطأ يُستخدم إذا أنشأت الطريقة [DaoException] جديدة؛
- الأسطر 4-7: الدور الأساسي لهذه الطريقة هو إلغاء المعاملة المرتبطة بالاتصال الذي تم تمريره كمعلمة 1؛
- الأسطر 8-10: إذا فشل إلغاء المعاملة، يتم تسجيل سجل الاستثناء على وحدة التحكم. لا يمكن القيام بأي شيء آخر نظرًا لأننا سنقوم بإطلاق استثناء في السطر 12؛
يتم إدارة finally للطرق المختلفة بالطريقة التالية:
// إدارة finally
protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
DaoException daoException) {
// الإغلاق ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
// إغلاق [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
// إغلاق الاتصال
if (connexion != null) {
try {
connexion.close();
} catch (SQLException e3) {
// يتم تسجيل الخطأ إن أمكن
if (daoException == null) {
daoException = new DaoException(code, e3, simpleClassName);
}
}
}
// النتيجة
return daoException;
}
- السطر 2: هذه الطريقة مُعلنة أيضًا بـ [protected]. وهي تستقبل المعلمات التالية:
- [ResultSet rs]: [ResultSet] المحتمل في حالة تنفيذ عملية [SELECT] — ربما null؛
- [PreparedStatement ps]: العملية [PreparedStatement] التي تم تنفيذها — ربما تكون null؛
- [Connection connexion]: الاتصال بـ SGBD - ربما null؛
- [int code]: رمز خطأ يُستخدم إذا أنشأت الطريقة [DaoException] جديدة؛
- [DaoException daoException]: قد يكون [DaoException] في حالة حدوث استثناء قبل finally - ربما null؛
- الأسطر 21-30: الهدف الأساسي من هذه الطريقة هو إغلاق الاتصال (السطر 23)؛
- الأسطر 24-29: إذا حدث استثناء أثناء عملية الإغلاق هذه، فإننا نتحقق من حالة المعلمة [DaoException daoException] التي تم تمريرها إلينا: إذا كانت [daoException == null]، فإننا نقوم بإنشاء معلمة جديدة [DaoException] باستخدام الرمز الذي تم تمريره كمعلمة؛
- السطر 32: يتم إرجاع المعلمة [DaoException] القديمة أو الجديدة كنتيجة؛
لن نعرض جميع طرق الفئة [Dao]، بل سنكتفي بعرض بعضها فقط. فهي جميعها متشابهة.
3.6.7.1. الطريقة [getProduitById]
تُرجع الطريقة [getProduitById] المنتج الذي يكون مفتاحه الأساسي مساويًا للمعلمة [id] أو null، وإلا؛
@Override
public Produit getProduitById(int id) {
// موارد الاتصال
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
// في البداية لا توجد استثناءات
DaoException daoException = null;
// المنتج المطلوب
Produit produit = null;
try {
// فتح الاتصال
connexion = dataSource.getConnection();
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة فقط
connexion.setReadOnly(true);
// قراءة الجدول [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_PRODUIT_BYID);
ps.setInt(1, id);
rs = ps.executeQuery();
if (rs.next()) {
produit = new Produit(id, rs.getString(1), rs.getInt(2), rs.getDouble(3), rs.getString(4));
}
// تثبيت المعاملة
connexion.commit();
// العودة إلى الوضع الافتراضي
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// معالجة الاستثناء
daoException = doCatchException(connexion, e1, 112);
} finally {
// معالجة جملة finally
daoException = doFinally(rs, ps, connexion, 113, daoException);
}
// استثناء؟
if (daoException != null) {
throw daoException;
}
// النتيجة
return produit;
}
- السطر 10: يتم تعيين المنتج المطلوب إرجاعه إلى null؛
- السطر 19: الأمر SQL [ConfigJdbc.V2_SELECT_PRODUIT_BYID] هو كما يلي:
public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";
- الأسطر 22-24: إذا كان [ResultSet] يحتوي على سطر، يتم استخدامه لإنشاء المنتج المطلوب إرجاعه، وإلا يظل المنتج المطلوب إرجاعه في null؛
- السطر 41: يتم إرجاع المنتج؛
- السطر 8: يتم تهيئة الاستثناء [DaoException] الخاص بالطريقة إلى null؛
- السطر 31: تُنشئ الطريقة [doCatchException] استثناءً [DaoException]؛
- السطر 34: المعلمة [daoException] للطريقة [doFinally] هي إما null, أو الاستثناء الذي أنشأته الطريقة [doCatchException]. الطريقة [doFinally]:
- تترك هذا المعلمة كما هي إذا تمكنت من إغلاق الاتصال؛
- تترك هذا المعامل كما هو إذا لم تتمكن من إغلاق الاتصال وكان قد تم إنشاء [DaoException] مسبقًا؛
- تنشئ عملية [DaoException] جديدة إذا لم تتمكن من إغلاق الاتصال ولم تكن هناك عملية [DaoException] من قبل؛
- الأسطر 37-39: إذا كان الاستثناء المحلي [daoException] لا يساوي null، يتم إطلاقه، وإلا يتم إرجاع النتيجة المطلوبة (السطر 41)؛
3.6.7.2. الطريقة [deleteProduits]
تقوم الطريقة [deleteProduits] بحذف المنتجات التي يتم تمرير مفاتيحها الأساسية كمعلمات إليها. وتُرجع عدد المنتجات المحذوفة.
@Override
public int deleteProduits(int[] ids) {
// موارد الاتصال
PreparedStatement ps = null;
Connection connexion = null;
// في البداية لا توجد استثناءات
DaoException daoException = null;
// عدد المنتجات التي تم تحديثها
int nbProduits = 0;
try {
// فتح الاتصال
connexion = dataSource.getConnection();
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة/الكتابة
connexion.setReadOnly(false);
// يتم حذف المنتجات
ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
for (int id : ids) {
// الإعدادات
ps.setInt(1, id);
// التنفيذ
nbProduits += ps.executeUpdate();
}
// تثبيت المعاملة
connexion.commit();
// العودة إلى الوضع الافتراضي
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// معالجة الاستثناء
daoException = doCatchException(connexion, e1, 171);
} finally {
// معالجة جملة finally
daoException = doFinally(null, ps, connexion, 172, daoException);
}
// استثناء؟
if (daoException != null) {
throw daoException;
}
// النتيجة
return nbProduits;
}
- السطر 18، الأمر SQL [ConfigJdbc.V2_DELETE_PRODUITS] هو كما يلي:
public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";
- الأسطر 18-24: كود حذف المنتجات. نلاحظ أن الأمر SQL يتم إعداده مرة واحدة (السطر 18) وتنفيذه n مرات (الأسطر 19-24). وهذا هو الغرض من الكائن [PreparedStatement]؛
- السطر 23: تُرجع الطريقة [PreparedStatement].executeUpdate() عدد الأسطر التي تأثرت بعملية التحديث؛
- السطر 41: يتم إرجاع عدد المنتجات التي تم تحديثها؛
3.6.7.3. الطريقة [updateProduits]
تقوم الدالة [updateProduits] بتحديث المنتجات التي يتم تمريرها إليها كمعلمات في قاعدة البيانات. وتُرجع عدد المنتجات التي تم تحديثها.
@Override
public int updateProduits(List<Produit> produits) {
// موارد الاتصال
PreparedStatement ps = null;
Connection connexion = null;
// في البداية لا توجد استثناءات
DaoException daoException = null;
// عدد المنتجات التي تم تحديثها
int nbProduits = 0;
try {
// فتح الاتصال
connexion = dataSource.getConnection();
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة/الكتابة
connexion.setReadOnly(false);
// يتم تحديث الجدول [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
for (Produit produit : produits) {
// المعلمات
ps.setString(1, produit.getNom());
ps.setDouble(2, produit.getPrix());
ps.setInt(3, produit.getCategorie());
ps.setString(4, produit.getDescription());
ps.setInt(5, produit.getId());
// التنفيذ
nbProduits += ps.executeUpdate();
}
// تثبيت المعاملة
connexion.commit();
// العودة إلى الوضع الافتراضي
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// معالجة الاستثناء
daoException = doCatchException(connexion, e1, 131);
} finally {
// معالجة جملة finally
daoException = doFinally(null, ps, connexion, 132, daoException);
}
// استثناء؟
if (daoException != null) {
throw daoException;
}
// النتيجة
return nbProduits;
}
- السطر 18: الترتيب SQL [ConfigJdbc.V2_UPDATE_PRODUITS] هو كما يلي:
public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
- الأسطر 19-28: كود تحديث المنتجات؛
3.6.7.4. الطريقة [addProduits]
تقوم الطريقة [addProduits] بإضافة المنتجات التي يتم تمريرها كمعلمات إلى قاعدة البيانات. وتُرجع هذه المنتجات نفسها مع مفاتيحها الأساسية (قبل الإدراج في قاعدة البيانات، لا تحتوي المنتجات على مفتاح أساسي).
@Override
public List<Produit> addProduits(List<Produit> produits) {
// موارد الاتصال
PreparedStatement ps = null;
Connection connexion = null;
// في البداية لا توجد استثناءات
DaoException daoException = null;
try {
// فتح الاتصال
connexion = dataSource.getConnection();
// في وضع القراءة/الكتابة
connexion.setReadOnly(false);
// بدء المعاملة
connexion.setAutoCommit(false);
// يتم إضافة عناصر إلى الجدول [PRODUITS]
String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
for (Produit produit : produits) {
// المعلمات
ps.setString(1, produit.getNom());
ps.setLong(2, produit.getCategorie());
ps.setDouble(3, produit.getPrix());
ps.setString(4, produit.getDescription());
// تنفيذ الأمر
ps.executeUpdate();
// تم إنشاء المفتاح الأساسي
ResultSet generatedKeys = ps.getGeneratedKeys();
if (generatedKeys.next()) {
produit.setId(generatedKeys.getInt(1));
} else {
throw new RuntimeException(String.format("Le produit de nom [%s] n'a pas récupéré de clé primaire",
produit.getNom()));
}
}
// تثبيت المعاملة
connexion.commit();
// العودة إلى الوضع الافتراضي
connexion.setAutoCommit(true);
} catch (SQLException | RuntimeException e1) {
// معالجة الاستثناء
daoException = doCatchException(connexion, e1, 151);
} finally {
// معالجة جملة finally
daoException = doFinally(null, ps, connexion, 152, daoException);
}
// استثناء؟
if (daoException != null) {
throw daoException;
}
// النتيجة
return produits;
}
- السطر 16، الترتيب SQL [ConfigJdbc.V2_INSERT_PRODUITS] هو كما يلي:
public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
في المثال أعلاه، لا يتضمن أمر إدراج المنتج المفتاح الأساسي [ID]. ونظرًا لأن المفتاح الأساسي لقاعدة البيانات MySQL له السمة [AUTOINCREMENT]، فإن SGBD سيقوم عندئذٍ بإنشاء مفتاح أساسي لكل عملية إدراج. وهنا تنشأ مشكلة استرداد هذه المفاتيح. وهذه نقطة مهمة لأن العمليات على المنتجات تتم عبر مفاتيحها الأساسية. لذا، من الضروري معرفة هذه المفاتيح؛
- الأسطر 17-33: حلقة إدراج المنتجات؛
- السطر 16: شكل خاص من طريقة [prepareStatement]. المعلمة الثانية [generatedColumns] هي مصفوفة من أسماء الأعمدة التي نريد استرداد قيمها بعد الإدراج. في السطر 16، أشرنا إلى أننا نريد استرداد قيمة العمود [id]. تجدر الإشارة هنا إلى أنه على الرغم من أن أسماء أعمدة الجدول لا تميز بين الأحرف الكبيرة والصغيرة، إلا أن SGBD PostgreSQL اشترط أن يكون هذا الاسم مكتوبًا بأحرف صغيرة. وهذا هو النوع المعتاد من المشاكل التي نواجهها عند نقل كود من SGBD إلى آخر؛
- السطر 24: إدراج سطر في قاعدة البيانات؛
- السطر 26: يتم استرداد قائمة قيم الأعمدة المحددة في السطر 16 في ملف [ResultSet]. هنا، بالنسبة لإدراج واحد، سيحتوي ملف [ResultSet] على سطر واحد، وسيحتوي هذا السطر على عمود واحد فقط يحتوي على المفتاح الأساسي؛
- السطر 28: يتم استرداد المفتاح الأساسي الذي تم إنشاؤه بواسطة SGBD؛
- الأسطر 29-32: إذا لم يتم الحصول على المفتاح الأساسي الذي تم إنشاؤه، يتم تشغيل [RuntimeException] الذي سيتم تغليفه في [DaoException] في الأسطر 38-40؛
3.6.8. الفئة [Dao2]
![]() |
![]() |
الفئة [Dao2] هي نسخة معدلة من الفئة [Dao1] تستخدم صيغة تسمى try-with-resource(resource):
- [resource] هي مورد يُنفذ واجهة [java.lang.AutoCloseable]. جميع الموارد التي يتم تحريرها باستخدام الطريقة [close] تندرج ضمن هذه الموارد. تضمن هذه الصيغة أن يتم إغلاق المورد [resource] في السطر 4. وهذا يغني عن كتابة جملة [finally] للقيام بمهمة الإغلاق هذه؛
لنأخذ على سبيل المثال الطريقة [getAllProduits] من الفئة [Dao2]:
@Override
public List<Produit> getAllProduits() {
// استثناء محتمل
DaoException daoException = null;
// قائمة المنتجات
List<Produit> produits = new ArrayList<Produit>();
try (Connection connexion = dataSource.getConnection()) {
// بدء المعاملة
connexion.setAutoCommit(false);
// في وضع القراءة فقط
connexion.setReadOnly(true);
// يتم قراءة الجدول [PRODUITS]
try (PreparedStatement ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_ALLPRODUITS)) {
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
produits.add(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
}
// نهاية المعاملة
connexion.commit();
// العودة إلى الوضع الافتراضي
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// إلغاء المعاملة
daoException = doRollback(connexion, e1, 111);
}
} catch (SQLException e2) {
// معالجة الاستثناء
if (daoException == null) {
daoException = new DaoException(112, e2, simpleClassName);
}
}
// استثناء؟
if (daoException != null) {
throw daoException;
}
// النتيجة
return produits;
}
- السطر 7: محاولة (try) باستخدام المورد [Connection]. في السطر 27، نضمن أن هذا المورد قد تم إغلاقه؛
- السطر 13: جملة try مع المورد [PreparedStatement]. في السطر 23، نضمن أن هذا المورد مغلق؛
- السطر 14: محاولة باستخدام المورد [ResultSet]. في السطر 19، نضمن أن هذا المورد مغلق؛
- السطر 25: يتم إلغاء المعاملة على النحو التالي:
private DaoException doRollback(Connection connexion, Throwable e1, int code) {
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e) {
e.printStackTrace();
}
// إنشاء الاستثناء
return new DaoException(code, e1, simpleClassName);
}
وفي النهاية، نحصل على كود أسهل في القراءة.
3.6.9. تنفيذ طبقة الاختبارات
3.6.9.1. فئات الاختبار
![]() |
![]() |
- الاختبار [JUnitTestDao1] هو اختبار JUnit من فئة [Dao1]؛
- الاختبار [JUnitTestDao2] هو اختبار JUnit من الفئة [Dao2]؛
- [AbstractJUnitTestDao] هي الفئة الأم للفئتين الاختباريتين السابقتين؛
- [MainTestDao1] هي فئة وحدة تحكم اختبار تابعة لفئة [Dao1]؛
- [MainTestDao2] هي فئة وحدة تحكم اختبار تابعة للفئة [Dao2]؛
- [AbstractMainTestDao] هي الفئة الأم للفئتين السابقتين. وهي تستند إلى كود فئات وحدة التحكم [IntroJdbc01, IntroJdbc02] التي سبق عرضها، ولذلك لن ندرس فئات وحدة التحكم هذه؛
الفئة [JUnitTestDao1] هي التالية:
package spring.jdbc.tests;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao1 extends AbstractJUnitTestDao {
// الطبقة [DAO]
@Autowired
@Qualifier("dao1")
private IDao dao;
@Override
IDao getDao() {
return dao;
}
}
- تم عرض التعليقات التوضيحية للسطرين 12 و13 في الفقرة 2.5.5. وهي تتيح لاختبار JUnit الوصول بسهولة إلى سياق Spring ومكوناته. ويتم تكوين هذا السياق بواسطة الفئة [AppConfig] (السطر 12) التي تمت دراستها في الفقرة 2.4.3؛
- السطر 14: تمتد هذه الفئة من الفئة [AbstractJUnitTestDao] التي سنعرضها لاحقًا. وتوجد في هذه الفئة طرق الاختبار JUnit؛
- الأسطر 17-19: يتم حقن الكائن المسمى [dao1] (السطر 18) (السطر 17). وبالتالي، فإن ما يتم حقنه هنا هو مثيل للفئة [Dao1]؛
- الأسطر 21-24: تعيد الطريقة [getDao] تعريف الطريقة التي تحمل الاسم نفسه في الفئة الأم؛
وفي النهاية، الهدف من هذه الفئة هو تزويد الفئة الأم بإشارة إلى الطبقة [DAO] التي يجب اختبارها، وهي هنا مثيل من [Dao1]. وبالمثل، تزود الفئة [JUnitTestDao2] الفئة الأم [AbstractJUnitTestDao] بمثيل من الفئة [Dao2].
الفئة [AbstractJUnitTestDao] هي فئة اختبار لفئة JUnit:
package spring.jdbc.tests;
import generic.jdbc.entities.dbproduits.Produit;
import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.BeansException;
import spring.jdbc.dao.IDao;
import spring.jdbc.infrastructure.DaoException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractJUnitTestDao {
// طبقة [DAO]
abstract IDao getDao();
// تعيين jSON
final static ObjectMapper jsonMapper = new ObjectMapper();
@Before
public void clean() {
// يتم تنظيف القاعدة قبل كل اختبار
log("Vidage de la base de données", 1);
getDao().deleteAllProduits();
}
@Test
public void getProduits() throws JsonProcessingException {
...
}
@Test
public void getProduitBy() {
...
}
@Test
public void doInsertsInTransaction() {
...
}
@Test
public void updateProduits() {
...
}
@Test
public void deleteProduits() {
....
}
@Test
public void perf1() {
...
}
@Test
public void perf2() {
...
}
@Test
public void perf3() {
....
}
// -------------- طرق خاصة
...
}
- السطر 19، الفئة [AbstractJUnitTestDao] هي فئة مجردة؛
- السطر 22: الطريقة المجردة [getDao] التي تتيح الحصول على مرجع للطبقة [DAO] المراد اختبارها. يتم تنفيذ هذه الطريقة بواسطة الفئات الفرعية؛
- السطر 25: مُخَطِّط jSON الذي سيسمح لنا بعرض قيمة jSON الخاصة بالمنتجات على وحدة التحكم؛
- الأسطر 27-32: قبل كل اختبار (السطر 27)، يتم إفراغ الجدول [PRODUITS]؛
3.6.9.2. الطريقة الخاصة [fill]
تُستخدم الطريقة الخاصة [fill] لإدراج المنتجات في الجدول [PRODUITS].
private List<Produit> fill(int nbProduits) {
log("Remplissage de la base de données", 1);
// يتم إنشاء قائمة بالمنتجات
List<Produit> produits = new ArrayList<Produit>();
for (int i = 0; i < nbProduits; i++) {
int n = i + 1;
// int id، String اسم، int فئة، double السعر، String الوصف
produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
"DESC%s", n)));
}
// يتم حفظها في قاعدة البيانات - يتم استرداد المنتجات باستخدام مفتاحها الأساسي
produits = getDao().addProduits(produits);
// يتم إنشاء قاموس للمنتجات لتسهيل العثور عليها
// مفتاح القاموس هو المفتاح الأساسي للمنتج في قاعدة البيانات
for (Produit produit : produits) {
mapProduits.put(produit.getId(), produit);
}
// نقوم بعرض المنتجات
return produits;
}
- السطر 1: تقوم الطريقة [fill] بإدراج [nbProduits] في الجدول [PRODUITS] الذي يُفترض أنه فارغ؛
- الأسطر 3-10: إنشاء قائمة بالمنتجات بالشكل التالي:
new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format("DESC%s", n)));
التي تستخدم منشئ المنتج (int id, String nom, int categorie, double prix, String description). لا تهم قيمة المعلمة الأولى [id] (المفتاح الأساسي للجدول [PRODUITS]) لا أهمية لها لأن الطريقة [addProduits] في السطر 10 لا تدخلها في قاعدة البيانات وتترك SGBD تولد قيمتها؛
- السطر 12: يتم حفظ قائمة المنتجات في قاعدة البيانات. ويتم إثراء كل منتج من منتجات هذه القائمة بمفتاح أساسي جديد هو [id]. وتُرجع الطريقة [addProduits] كنتيجة معلمتها [produits]. لذلك كان من الممكن عدم استرداد النتيجة؛
- السطور 15-17: يتم وضع المنتجات في قاموس:
// قاموس المنتجات
private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();
مفتاح القاموس هو المفتاح الأساسي للمنتج والقيمة المرتبطة به، وهي المنتج نفسه؛
- السطر 19: نُرجع قائمة المنتجات؛
3.6.9.3. الاختبار [getProduits]
وهو كما يلي:
@Test
public void getProduits() throws JsonProcessingException {
// ملء
fill(10);
// قائمة المنتجات
log("Liste des produits", 2);
List<Produit> produits = getDao().getAllProduits();
affiche(produits);
// نتحقق من أن القائمة المستردة والقائمة المخزنة متطابقتان
for (Produit produit : produits) {
Produit found = mapProduits.get(produit.getId());
Assert.assertEquals(found, produit);
mapProduits.remove(found.getId());
}
// يجب أن تكون جميع المنتجات الأصلية قد اختفت من القاموس
Assert.assertEquals(0, mapProduits.size());
}
}
- السطر 4: يتم إدراج 10 منتجات في قاعدة البيانات؛
- السطر 7: بعد ذلك، نطلب عرض جميع المنتجات الموجودة في قاعدة البيانات؛
- السطر 8: يتم عرضها. الهدف هو التأكد من أن المنتجات قد تم تسجيلها بالفعل وأن لها مفتاحًا أساسيًّا؛
- الأسطر 10-13: نتحقق من أن المنتجات التي تم العثور عليها مطابقة لتلك التي تم حفظها والتي يمكن العثور عليها في القاموس [mapProduits]؛
- السطر 11: نسترد من القاموس المنتج الذي يحمل نفس المفتاح الأساسي للمنتج الذي تم استرجاعه من قاعدة البيانات. وهذا يوضح أن المنتجات التي تم حفظها قد حصلت بالفعل على مفتاح أساسي؛
- السطر 12: نتأكد من أن المنتجين متطابقان. نذكر أن الفئة [Produit] قد عرّفت طريقة [equals] (انظر الفقرة 3.3.4)؛
- السطر 13: يتم حذف العنصر الذي تم العثور عليه من القاموس؛
- السطر 16: نتحقق من أن قاموس المنتجات الأولية فارغ بالفعل، مما يعني أن هذه المنتجات الأولية كانت جميعها موجودة في قائمة المنتجات المسترجعة من قاعدة البيانات؛
الطريقة [affiche] في السطر 8 هي الطريقة الخاصة التالية:
// عرض قائمة المنتجات
private <T> void affiche(List<T> elements) throws JsonProcessingException {
for (T element : elements) {
System.out.println(jsonMapper.writeValueAsString(element));
}
}
- السطر 2: الطريقة [affiche] هي طريقة عامة. يتم تحديد معلماتها بنوع T يُرمز له نحويًّا بـ <T>. لو كانت معلماتها نوعين هما T1 و T2، لكُتبت على النحو التالي: <T1,T2>. تكون صيغة الدالة m التي تُحدد معلماتها بنوع T كما يلي:
في كود الدالة m، سنجد بيانات من النوع T. ويمكن عندئذٍ استدعاء الدالة m لمثيل c من فئة C بالطريقة التالية:
حيث T1 هو النوع الفعلي الذي سيحل محل النوع الشكلي T للطريقة m. في معظم الأحيان، يستطيع المُجمِّع استنتاج النوع T1 بناءً على معلمات الطريقة m. ولذلك، غالبًا ما يتم تبسيط التعليمات السابقة إلى:
لنعد إلى الطريقة [affiche]. فهي تعرض قائمة بعناصر من النوع T. وهذا ممكن لأن المُخَطِّط jSON المستخدم في السطر 4 قادر على تقديم التمثيل jSON لأي نوع من الكائنات. في هذا المثال بالتحديد، سيكون النوع T الوحيد المستخدم هو النوع [Produit].
كان من الممكن أيضًا كتابة الطريقة [affiche] بالطريقة التالية:
// عرض قائمة المنتجات
private void affiche(Object o) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(o));
}
ونظرًا لأن المعلمة الفعلية عبارة عن قائمة بالمنتجات، فإن السطر 3 كان سيكتب التمثيل jSON لهذه القائمة. وهذا يختلف عن كتابة تمثيل كل عنصر من عناصرها واحدًا تلو الآخر.
العرض الناتج عن الاختبار [getProduits] هو كما يلي:
3.6.9.4. الاختبار [getProduitBy]
وهو كما يلي:
@Test
public void getProduitBy() {
// التعبئة
fill(10);
log("getProduitBy", 1);
Produit produit = getDao().getProduitByName("NOM3");
Produit produit2 = getDao().getProduitById(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
}
- السطر 6: تُستخدم الطريقة [getProduitByName] التابعة للواجهة [IDao] لاسترداد المنتج الذي يحمل الاسم [NOM3]؛
- السطر 7: تُستخدم بعد ذلك الطريقة [getProduitById] التابعة للواجهة [IDao] لاسترداد المنتج نفسه، الذي يتم تحديده هذه المرة بواسطة مفتاحه الأساسي؛
- الأسطر 8-10: يتم التحقق من أن [produit2] و [produit] لهما نفس الخصائص؛
3.6.9.5. اختبار [doInsertsInTransaction]
وهو كما يلي:
@Test
public void doInsertsInTransaction() {
log("Ajout de deux produits de même nom", 1);
// يتم الإدراج
List<Produit> inserts = new ArrayList<Produit>();
inserts.add(new Produit(0, "x", 1, 1.0, ""));
inserts.add(new Produit(0, "x", 1, 1.0, ""));
boolean erreur = false;
try {
getDao().addProduits(inserts);
} catch (DaoException daoException) {
erreur = true;
}
// عمليات التحقق
Assert.assertTrue(erreur);
List<Produit> produits = getDao().getAllProduits();
Assert.assertEquals(0, produits.size());
}
- الأسطر 5-7: يتم إنشاء قائمة من منتجين يحملان نفس الاسم [x]؛
- السطر 10: يتم إدراج هذين المنتجين في الجدول [PRODUITS] الذي يكون فارغًا (الطريقة [clean] المُعلَّمة بـ [@Before]). سيتم تنفيذ عملية الإدراج الأولى، لكن لن يتم تنفيذ الثانية لأن الجدول [PRODUITS] يخضع لقيود الفرادة على أسماء المنتجات. وبالتالي، لا بد أن تحدث استثناء. ويتم اختبار هذا الاستثناء في السطر 15؛
- ونظرًا لأن جميع أساليب واجهة [IDao] تُنفَّذ داخل معاملة، فإن فشل عملية الإدراج الثانية سيؤدي إلى إلغاء المعاملة بأكملها، وبالتالي إلغاء عملية الإدراج الأولى. وفي النهاية، لن تتم أي عملية إدراج في الجدول [PRODUITS]؛
- السطران 16-17: يتم التحقق من هذه النقطة عن طريق طلب قائمة المنتجات الموجودة في الجدول [PRODUITS] والتأكد من أن هذه القائمة فارغة؛
3.6.9.6. اختبار [updateProduits]
وهو كما يلي:
@Test
public void updateProduits() {
// التعبئة
fill(10);
log("Mise à jour du prix des produits de catégorie 1", 1);
// استرداد المنتجات
List<Produit> produits = getDao().getAllProduits();
// تحديث المنتجات من الفئة 1
List<Produit> updated = new ArrayList<Produit>();
int nbUpdated = 0;
for (Produit produit : produits) {
if (produit.getCategorie() == 1) {
// int id، String اسم، int الفئة، double السعر، String الوصف
updated
.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
nbUpdated++;
}
}
int nbProduits = getDao().updateProduits(updated);
// عمليات التحقق
// Assert.assertEquals(nbUpdated, nbProduits); -- لا يعمل مع DB2
for (Produit produit : updated) {
Produit produit2 = getDao().getProduitById(produit.getId());
Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
}
}
- السطر 4: نضيف 10 منتجات إلى قاعدة البيانات؛
- السطر 7: نستردها؛
- الأسطر 9-18: نرفع أسعار منتجات الفئة رقم 1 بنسبة 10%؛
- السطر 19: يتم تسجيل هذه التعديلات في قاعدة البيانات؛
- الأسطر 22-25: يتم استعراض قائمة المنتجات التي استُخدمت في التحديث في الذاكرة. وبالنسبة لكل منتج منها، يتم البحث في قاعدة البيانات عن المنتج الذي يحمل نفس المفتاح الأساسي والتحقق من أن تحديث السعر قد تم بالفعل؛
- السطر 19: يتم استرداد عدد المنتجات التي تم تحديثها بواسطة العملية [updateProduits]؛
- السطر 21: يتم التحقق من أن هذا العدد هو العدد المتوقع بالفعل. ينجح هذا الاختبار لجميع عمليات SGBD باستثناء SGBD وDB2. ولذلك تم وضعها في التعليقات؛
3.6.9.7. اختبار [deleteProduits]
وهو كما يلي:
@Test
public void deleteProduits() {
// التعبئة
fill(10);
log("deleteProduits", 1);
// قائمة المنتجات
List<Produit> produits = getDao().getAllProduits();
// حذف منتجين
Produit produit0 = produits.get(0);
Produit produit5 = produits.get(5);
int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
// عمليات التحقق
// Assert.assertEquals(2, nbDeleted)؛ -- لا يعمل مع DB2
Assert.assertNull(getDao().getProduitById(produit0.getId()));
Assert.assertNull(getDao().getProduitById(produit5.getId()));
Assert.assertEquals(produits.size() - 2, getDao().getAllProduits().size());
}
- السطر 4: نضيف 10 منتجات إلى قاعدة البيانات؛
- الأسطر 7-11: نسترد جميع المنتجات الموجودة في قاعدة البيانات ونحذف منها المنتجات المستردة في الموضعين 0 و5؛
- الأسطر 14-16: نتحقق من أن المنتجين لم يعودا موجودين في قاعدة البيانات وأن عدد المنتجات فيها قد انخفض بمقدار منتجين؛
- لا يجتاز الاختبار في السطر 13 مع SGBD وDB2. لكنه يجتازه مع SGBD؛
3.6.9.8. اختبارات الأداء
أدرجنا في الاختبارات ثلاث طرق لا تهدف سوى إلى تقييم أداء SGBD:
@Test
public void perf1() {
// ملء
fill(10000);
}
@Test
public void perf2() {
// ملء
fill(10000);
// تعديل
List<Produit> produits = getDao().getAllProduits();
// يتم تحديث تلك التي تنتمي إلى الفئة 1
List<Produit> updated = new ArrayList<Produit>();
for (Produit produit : produits) {
// int id، String اسم، int الفئة، double السعر، String الوصف
updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
}
getDao().updateProduits(updated);
}
@Test
public void perf3() {
// التعبئة
fill(10000);
// حذف
List<Produit> produits = getDao().getAllProduits();
// المفاتيح الأساسية
int[] keys = new int[produits.size()];
for (int i = 0; i < keys.length; i++) {
keys[i] = produits.get(i).getId();
}
getDao().deleteProduits(keys);
}
- الأسطر 1-5: إدراج 10000 منتج؛
- الأسطر 8-20: إدراج 10000 منتج ثم تعديلها عبر مفاتيحها الأساسية؛
- الأسطر 23-34: إدراج 10000 منتج ثم حذفها عبر مفاتيحها الأساسية؛
لتنفيذ الاختبارين [JUnitTestDao1] و [JUnitTestDao2]، يمكن استخدام إعدادات التنفيذ التالية:
![]() | ![]() |
فيما يلي نتائج الاختبار [JUnitTestDao1]:
![]() |
في [1] تظهر نتائج [JUnitTestDao1]، وفي [2] تظهر نتائج [JUnitTestDao2]. ولا توجد فروق ذات دلالة إحصائية بينها. في [1]:
- تم اجتياز الاختبار بنجاح؛
- يستغرق إدراج 10000 منتج 3,15 ثانية؛
- يستغرق إدراج 10000 منتج ثم تعديلها 4,80 ثانية؛
- يستغرق إدراج 10000 منتج ثم حذفها 4,40 ثانية؛
- وبالتالي فإن العملية الأكثر استهلاكًا للموارد هي الإدراج؛



















































