Skip to content

3. مقدمة إلى واجهة برمجة تطبيقات JDBC

3.1. إعداد بيئة العمل

سنعمل مع قاعدة بيانات MySQL5.

يجب أن يكون لديك:

نفترض فيما يلي أن مسؤول MySQL5 هو root وكلمة المرور هي root. قم بتشغيل نظام إدارة قواعد البيانات MySQL5 وعميله [MyManager]. باستخدام [MyManager]، نقوم بإنشاء قاعدة البيانات [dbproduits] [1-34]:

  • في [3]، يجب تسمية قاعدة البيانات [dbproduits
  • في [8-9]، قم بتسجيل الدخول باستخدام كلمة مرور الجذر (التي لا تظهر في لقطة الشاشة أعلاه)؛
  • في [14a]، كلمة المرور هي root مرة أخرى (والتي لا تظهر في لقطة الشاشة)؛
  • في [15]، تم إنشاء قاعدة البيانات [dbproduits
  • في [20]، انتبه إلى قاعدة البيانات المحددة. يجب أن تكون قاعدة البيانات [dbproduits
  • في [22]، المجلد هو <examples>/spring-database-config/mysql/databases، حيث <examples> هو المجلد الذي يحتوي على الأمثلة التي تم تنزيلها؛
  • في [23]، حدد البرنامج النصي SQL [dbproduits.sql]. سيؤدي ذلك إلى إنشاء الجدول [PRODUITS] في قاعدة البيانات [dbproduits
  • في [30]، تم إنشاء جدول [المنتجات]؛
  • في [33]، أعمدة جدول [المنتجات]؛
  • في [34]، يكون فارغًا في البداية؛

الآن، باستخدام STS، قم باستيراد المشاريع التالية (اتبع نفس الإجراء المستخدم للمشاريع الموجودة في المجلد <examples>/spring-core):

  • في [2]، سيتم العثور على مشروع [mysql-config-jdbc] في المجلد [<examples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1]؛

يقوم هذا المشروع بتكوين طبقة JDBC للبنية التالية:

ثم قم باستيراد المشاريع الثلاثة التالية مرة أخرى:

  • في [2]، ستجد المشاريع في المجلد [<examples>/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. خطوات تشغيل قاعدة البيانات

في البنية المذكورة أعلاه، يتضمن تشغيل قاعدة البيانات عبر برنامج وحدة التحكم الخطوات التالية:

  1. تحميل برنامج تشغيل JDBC الخاص بقاعدة البيانات؛
  1. فتح اتصال بقاعدة البيانات؛
  2. تنفيذ استعلام SQL على قاعدة البيانات ومعالجة نتائج استعلام SQL؛
  3. إغلاق الاتصال؛

يتم تنفيذ الخطوة 1 مرة واحدة فقط. يتم تنفيذ الخطوات 2-4 بشكل متكرر. لاحظ أن الاتصالات لا تُترك مفتوحة؛ بل يتم إغلاقها بمجرد انتهاء الحاجة إليها.

3.2.1. الخطوة 1 - تحميل برنامج تشغيل JDBC في الذاكرة

الرمز


        // driver loading JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
            // handle the exception
}

الغرض من العملية في السطر 3 هو تحميل برنامج تشغيل JDBC الخاص بقاعدة البيانات في الذاكرة. لا يلزم تنفيذ هذه العملية سوى مرة واحدة. ومع ذلك، فإن تكرارها لا يتسبب في حدوث خطأ. يتم البحث عن فئة برنامج تشغيل JDBC في مسار فئات المشروع. لذلك، في مشروع Eclipse، يجب أن يكون ملف [jar] الذي يحتوي على فئة برنامج تشغيل JDBC قد تم تضمينه في مسار فئات المشروع.

3.2.2. الخطوة 2 - فتح اتصال

بمجرد تثبيت برنامج تشغيل JDBC، نطلب منه فتح اتصال بقاعدة البيانات:

الكود


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 {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // we handle the exception
            ...
        } finally {
         // close connection
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // handle the exception
                ...
            }
         }
}
  • الأسطر 3–7: الفئات التي تنفذ واجهة JDBC موجودة جميعها في الحزمة [java.sql]. علاوة على ذلك، في حالة حدوث خطأ، فإنها جميعًا ترمي استثناء [SQLException] (الأسطر 19 و27). هذا الاستثناء مشتق من فئة [Exception] وهو ما يُسمى باستثناء مُتحقق منه: يجب عليك استخدام كتلة try/catch لمعالجته، أو بدلاً من ذلك، اختيار عدم معالجته والإشارة إلى أن الطريقة تسمح بانتشار الاستثناء عن طريق إضافة [throws SQLException] إلى توقيع الطريقة؛
  • السطر 17، [DriverManager.getConnection] هي طريقة ثابتة تتوقع ثلاثة معلمات:
    • [url]: عنوان URL لقاعدة البيانات. هذه سلسلة تعتمد على قاعدة البيانات المستخدمة. بالنسبة لـ MySQL، تكون على شكل [jdbc:mysql://localhost:3306/db_name
    • [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 {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            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)));
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
             doCatchException(connexion,e1);
        } finally {
            // we treat the finally
            doFinally(rs, ps, connexion);
        }
 
    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • السطران 8 و 10: فتح معاملة (السطر 8) في وضع القراءة فقط (السطر 10). المعاملة هي سلسلة من عبارات SQL التي إما تنجح جميعها أو تفشل جميعها. وبالتالي، في معاملة تحتوي على N عبارة SQL، إذا فشلت العبارة (I+1)، فسيتم التراجع عن العبارات I السابقة. بالنسبة لعملية القراءة، لا تكون المعاملة ضرورية. ومع ذلك، فإن إنشاء معاملة للقراءة فقط يمكن أن يسمح لبعض أنظمة إدارة قواعد البيانات (DBMS) بإجراء بعض التحسينات؛
  • السطر 12: استخدام [PreparedStatement]. عادةً ما تحتوي [PreparedStatement] على معلمات يُشار إليها بالحرف ?. هنا، لا تحتوي على أي معلمات. [PreparedStatement] هو عبارة تم إعدادها بواسطة نظام إدارة قواعد البيانات (DBMS). هذا الإعداد له تكلفة ويتم تنفيذه مرة واحدة فقط. ثم يتم تنفيذ هذه العبارة المعدة بواسطة نظام إدارة قواعد البيانات (DBMS) باستخدام المعلمات الفعلية التي تحل محل المعلمات المؤقتة ?. لاحظ أنه من الأفضل تحديد الأعمدة المطلوبة بدلاً من استخدام الرمز * لاسترداد جميع الأعمدة. من خلال تحديد أسماء الأعمدة، يمكن بعد ذلك استرداد قيمها بناءً على موضعها في عبارة SELECT؛
  • السطر 13: تنفيذ [PreparedStatement]. يتم استرداد كائن [ResultSet

يمثل كائن [ResultSet] جدولًا، أي مجموعة من الصفوف والأعمدة. في أي وقت معين، لا يمكننا الوصول إلا إلى صف واحد من الجدول، يُسمى الصف الحالي. عند إنشاء [ResultSet] في البداية، لا يوجد صف حالي. يجب علينا تنفيذ عملية [ResultSet.next()] للحصول عليه. توقيع طريقة next هو كما يلي:

    boolean 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
  • سيتم نقل عمود [NAME] إلى عمود في [ResultSet] باسم [myName
  • ...

في المثال أعلاه، يُطلق على المعرفات [myCol] اسم تسميات الأعمدة. بدون هذه التسميات، تعتمد أسماء أعمدة [ResultSet] على نظام إدارة قواعد البيانات (DBMS). عندما يعمل [SELECT] على جدول واحد، تكون تسميات الأعمدة افتراضيًا هي أسماء الأعمدة المطلوبة بواسطة SELECT. تنشأ المشكلة عندما يعمل [SELECT] على جداول متعددة وتحتوي تلك الجداول على أسماء أعمدة متطابقة، كما في المثال التالي:

SELECT PRODUITS.NOM, CATEGORIES.NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

بافتراض أن الجدول [PRODUCTS] يحتوي على مفتاح خارجي للجدول [CATEGORIES] ممثلاً بالعلاقة [PRODUCTS].CATEGORY_ID --> [CATEGORIES].ID، وأن كلا الجدولين [PRODUCTS] و[CATEGORIES] يحتويان على حقل [NAME]. في هذه الحالة، تعتمد الأسماء المحددة في [ResultSet] لأعمدة [PRODUITS.NOM] و [CATEGORIES.NOM] على نظام إدارة قواعد البيانات (DBMS). ولضمان قابلية النقل بين أنظمة إدارة قواعد البيانات، يجب استخدام تسميات الأعمدة هنا، وسنكتب:


SELECT PRODUITS.NOM as p_NOM, CATEGORIES.NOM as c_NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

للوصول إلى الحقول المختلفة للصف الحالي في [ResultSet]، تتوفر الطرق التالية:

Type getType("labelColi") 

لاسترداد العمود المسمى "labelColi" من الصف الحالي، أي العمود في عبارة [SELECT] الذي يحمل هذا الاسم. يشير النوع إلى نوع بيانات حقل "labelColi". يمكن استخدام طرق [getType] التالية: getInt، getLong، getString، getDouble، getFloat، getDate، ... بدلاً من استخدام اسم العمود، يمكنك استخدام موضعه في استعلام [SELECT] الذي تم تنفيذه:

Type getType(i) 

حيث i هو مؤشر العمود المطلوب (i>=1).

  • الأسطر 15–17: استرجاع القيم المقروءة من قاعدة البيانات؛
  • السطر 19: يتم التحقق من صحة المعاملة (المعروف أيضًا باسم الالتزام). يؤدي هذا إلى إنهائها وتحرير الموارد التي خصصها نظام إدارة قواعد البيانات لها؛
  • السطر 25: يتم تحرير الموارد في كتلة [finally]. وهذا يستدعي الطريقة [doFinally] التالية:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // closure ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {
 
            }
        }
        // closure [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {
 
            }
        }
        if (connexion != null) {
            try {
                // close connection
                connexion.close();
            } catch (SQLException e3) {
                // handle the exception
            }
        }
    }
  • الأسطر 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) {
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // handle the exception
        }
}
  • الأسطر 4–6: يتم التراجع عن المعاملة. يؤدي ذلك إلى إنهائها، ويمكن لنظام إدارة قواعد البيانات (DBMS) تحرير الموارد المخصصة لها؛

3.2.4. الخطوة 3 - إصدار أوامر SQL [INSERT، UPDATE، DELETE]

بيانات SQL [INSERT، UPDATE، DELETE] هي عمليات تحديث: فهي تُعدّل قاعدة البيانات ولكنها لا تُرجع أي صفوف. والمعلومة الوحيدة التي تُرجع هي عدد الصفوف التي تأثرت بعملية التحديث.

الكود


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // en mode lecture / écriture
            connexion.setReadOnly(false);
            // on met à jour la table
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // catégorie 1
            ps.setInt(1, 10);
            // exécution
            int nbLignes=ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException(connexion, e1);
        } finally {
            // on traite le 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 - إغلاق الاتصال

يجب إغلاق الاتصال بأسرع ما يمكن في بيئة متعددة المستخدمين لأن نظام إدارة قواعد البيانات (DBMS) يقبل عددًا محدودًا من الاتصالات المفتوحة. في الأمثلة السابقة، تم إغلاقه في جملة [finally] لعمليات SQL بحيث يتم إغلاقه بغض النظر عما إذا حدث استثناء أم لا.

3.3. تكوين طبقة JDBC لنظام إدارة قواعد البيانات 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>
        <!-- dépendances variables ********************************************** -->
        <!-- driver JDBC from SGBD -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- dépendances constantes ********************************************** -->
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- library 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 Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- logs -->
        <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: عنصر Maven الذي تم إنشاؤه بواسطة المشروع. كما ذكرنا سابقًا، تقوم جميع مشاريع [*-config-jdbc] بإنشاء هذا العنصر نفسه. لذلك، يجب ألا يتم تحميل مشروعين من مشاريع [*-config-jdbc] في نفس الوقت؛
  • الأسطر 9–13: مشروع Maven الأصلي لهذا المشروع. وهو يحدد إصدارات عدد كبير من الأرشيفات المستخدمة في نظام Spring. وهذا يتجنب الحاجة إلى تحديدها في المشاريع المشتقة منه؛
  • الأسطر 18–21: أرشيف برنامج تشغيل JDBC لنظام إدارة قواعد البيانات 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 {
 
    // paramètres de connexion
    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";
...
    // ordres 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')";
 
    // ordres 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 و 02]؛
  • الأسطر 28–34: عبارات SQL المستخدمة في مشروع [spring-jdbc-03

تستخدم عبارات SQL هذه الجدول [PRODUCTS] في قاعدة بيانات MySQL5 [dbproducts]، التي تتمتع بالبنية التالية:

 
  • [ID]: المفتاح الأساسي في وضع AUTO_INCREMENT (إذا لم يتم تحديد مفتاح أساسي، يقوم نظام إدارة قواعد البيانات بإنشائه)؛
  • [NAME]: اسم المنتج — فريد؛
  • [CATEGORY]: رقم الفئة؛
  • [PRICE]: سعره؛
  • [DESCRIPTION]: وصف المنتج؛

3.3.4. فئة [Product]

  

تمثل فئة [Product] صفًا في جدول [PRODUCTS]:


package generic.jdbc.entities.dbproduits;
 
public class Produit {
 
    // fields
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;
 
    // manufacturers
    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;
    }
 
    // getters and setters
...
}

لاحقًا، سنحتاج إلى مقارنة منتجين لتحديد ما إذا كانا متساويين أم لا. سنقول إن المنتجين متساويان إذا كانت جميع حقولهما متساوية. للقيام بذلك، سنقوم بتجاوز طريقة [equals] الخاصة بفئة [Object]، التي تنبثق منها فئة [Product]:


    // méthode d'égalité
    @Override
    public boolean equals(Object o) {
        // cas simples
        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⁻⁶؛

علاوة على ذلك، سنعيد تعريف طريقة [hashCode] لفئة [Object]:


    // hashcode
    @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 {
 
    // ajouter des produits
    public List<Produit> addProduits(List<Produit> produits) throws SQLException;
 
    // liste de tous les produits
    public List<Produit> getAllProduits() throws SQLException;
 
    // un produit particulier
    public Produit getProduitById(int id) throws SQLException;
 
    public Produit getProduitByName(String name) throws SQLException;
 
    // mise à jour de plusieurs produits
    public int updateProduits(List<Produit> produits) throws SQLException;
 
    // suppression de tous les produits
    public int deleteAllProduits() throws SQLException;
 
    // suppression de plusieurs produits
    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;
 
// generic exception class
// the exception is uncontrolled
 
public class UncheckedException extends RuntimeException {
 
    // serial ID generated
    private static final long serialVersionUID = -2924871763340170310L;
 
    // properties
    private int code;
    private String trace;
    private List<ShortException> exceptions;
 
    // manufacturers
    public UncheckedException() {
        super();
    }
 
    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // local
        this.code = code;
        this.exceptions = getErreursForException(e);
        // trace
        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);
    }
 
    // list of exception error messages
    private List<ShortException> getErreursForException(Throwable th) {
        // retrieve the elements of the exception stack
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // retrieve the current exception
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // following exception
            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;
        }
    }
 
    // getters and setters
...
}
  • السطر 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 للمشروع:

        <!-- library 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 {
 
    // properties
    private String className;
    private String errorMessage;
 
    // manufacturers
    public ShortException() {
 
    }
 
    public ShortException(String className, String errorMessage) {
        this.className = className;
        this.errorMessage = errorMessage;
    }
 
    // getters and setters
...
}
  • السطر 6: اسم فئة الاستثناء الذي حدث؛
  • السطر 7: رسالة الخطأ المرتبطة؛

دعونا نفحص المنشئ التالي لفئة [UncheckedException]:


    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // local
        this.code = code;
        this.exceptions = getErreursForException(e);
        // trace
        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] هو المعلمة الثالثة للمُنشئ. الفكرة هي تسجيل المكان الذي حدث فيه الاستثناء في كود المستخدم. سيقوم كود المستخدم بتغليف الاستثناء على النحو التالي:
1
2
3
4
5
6
7
try{
// code qui peut lancer une exception contrôlée
...
}catch(UnTypeDexception e){
// on encapsule l'exception contrôlée e dans une exception non contrôlée
    throw new UncheckedException(189,e,getClass().getSimpleClassName())
}
  • السطر 13: نقوم بإنشاء سلسلة من النوع [fileName, methodName, lineNumber] تصف الموقع في كود المستخدم حيث تم التقاط الاستثناء e؛

الآن، دعونا نفحص الكود الذي يسجل قائمة الاستثناءات من مكدس الاستثناءات للاستثناء [Throwable th] الذي تم تغليفه بواسطة المنشئ السابق:


    // liste des messages d'erreur d'une exception
    private List<ShortException> getErreursForException(Throwable th) {
        // on récupère les éléments de la pile de l'exception
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // on récupère l'exception courante
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // exception suivante
            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 /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- configuration JDBC of 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 للمشاريع التي تعتمد على المشروع الذي تمت إزالته من [مستكشف الحزم]. يؤدي هذا إلى تغيير فرع [تبعيات Maven] لهذه المشاريع. على سبيل المثال، بالنسبة لمشروع [spring-jdbc-01]، يصبح فرع [تبعيات Maven] كما يلي:

هذه المرة، لم تعد التبعية على مشروع بل على أداة Maven الخاصة به، وهي في هذه الحالة أداة [generic-config-jdbc] [1]. يمكننا أن نرى أننا نتمتع بالفعل بإمكانية الوصول إلى جميع الفئات والواجهات الخاصة بهذه الأداة. كما ذكرنا، سيتم إنشاء هذه الأداة بواسطة جميع مشاريع [*-config-jdbc]. لتجنب الأخطاء، نقوم بما يلي:

  • نحتفظ دائمًا بمشروع [*-config-jdbc] واحد في علامة التبويب [Package Explorer
  • نقوم بتحديث تكوين Maven لجميع المشاريع في علامة التبويب [Package Explorer] (Alt-F5) بحيث تتضمن مشروع [*-config-jdbc] في تبعيات Maven الخاصة بها؛

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 {
 
    // constants
    final static ObjectMapper jsonMapper = new ObjectMapper();
 
    public static void main(String[] args) {
        // loading the JDBC driver from SGBD
        try {
            Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // empty table [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // fill it
        System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
        insert();
        // we read it
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // update
        System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
        update();
        // display
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // empty table [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // we display it
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // INSERTion of two identical elements
        // the INSERTion must fail and neither element is inserted because of the transaction
        System.out.println(String.format("------------------------------ %s",
                "Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
        insert2();
        // we check
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // finish
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }
 
    // product list
    private static void select() {
    ...
    }
 
    // display jSON of an object
    private static void affiche(Object object) {
...
    }
 
    // product deletion
    public static void delete() {
...
    }
 
    // add products
    public static void insert() {
...
    }

    // add 2 products with the same primary keys
    public static void insert2() {
...
    }
 
    // product updates
    public static void update() {
...
    }
 
    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // closure ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {
 
            }
        }
        // closure [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {
 
            }
        }
        if (connexion != null) {
            try {
                // close connection
                connexion.close();
            } catch (SQLException e3) {
                // display error msg
                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) {
        // display error msg
        show(title, getErreursFromThrowable(th));
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // display error msg
            show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
        }
    }
 
    private static List<String> getErreursFromThrowable(Throwable th) {
        // retrieve the list of exception error msgs
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // throwable error message
            erreurs.add(th.getMessage());
            // we move on to the cause of throwable
            th = th.getCause();
        }
        // result
        return erreurs;
    }
 
    private static void show(String title, List<String> messages) {
        // title
        System.out.println(String.format("%s : ", title));
        // messages
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
}
  • الأسطر 23–29: تحميل برنامج تشغيل JDBC لنظام إدارة قواعد البيانات. في السطر 25، يتم استخدام الثابت [ConfigJdbc.DRIVER_CLASSNAME] المُعرَّف في مشروع [mysql-config-jdbc
  • الأسطر 136–147: تُرجع الطريقة [getErrorsFromThrowable] قائمة رسائل الخطأ المُغلفة في كائن من النوع [Throwable]، وهو الفئة الأم لفئة [Exception]. قد يحتوي الاستثناء على استثناء آخر، والذي يمكن استرداده باستخدام طريقة [Throwable].getCause(). وهذا يسمح لنا بالتكرار عبر جميع الاستثناءات المغلفة في كائن [Throwable
  • الأسطر 149–156: تعرض الطريقة [show(String title, List<String> messages)] الرسائل مسبوقة بالنص [title
  • الأسطر 122–134: تعالج الطريقة [doCatchException(String title, Connection connection, Throwable th)] الاستثناءات التي تواجهها طرق الفئة. ويمثل الاستثناء المعالج المعلمة [Throwable th]. والغرض من هذه الطريقة هو:
    • التراجع عن المعاملة الحالية للكائن [Connection connection] (الأسطر 127–129)؛
    • كتابة رسائل الخطأ المضمنة في الاستثناء [Throwable th] (الأسطر 124، 132)؛
  • الأسطر 93–120: تعالج الطريقة [doFinally(ResultSet rs, PreparedStatement ps, Connection connection)] كتلة [finally] لطرق الوصول إلى نظام إدارة قواعد البيانات. والغرض منها هو تحرير الموارد المخصصة بواسطة الاتصال؛

3.4.4. حذف محتويات جدول المنتجات

تحذف الطريقة [delete] محتويات الجدول:


    // product deletion
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // empty table [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // we treat the 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 التي يجب أن تنجح جميعها أو يتم التراجع عنها جميعًا. هناك أربع عمليات يجب الانتباه إليها:

  • بدء المعاملة: [connection.setAutoCommit(false)];
  • نهاية معاملة ناجحة: [connection.commit()]. في هذه الحالة، يتم تثبيت جميع العمليات التي تم إجراؤها على قاعدة البيانات أثناء المعاملة؛
  • نهاية معاملة فاشلة: [connection.rollback()]. في هذه الحالة، يتم التراجع عن جميع العمليات التي تم إجراؤها على قاعدة البيانات أثناء المعاملة؛

في أمثلةنا، كلما حدث استثناء، نقوم بإلغاء المعاملة في طريقة [doCatchException]:


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // display error msg
        Static.show(title, Static.getErreursFromThrowable(th));
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // display error msg
            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 {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // fill the table
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
            for (int i = 0; i < 10; i++) {
                // preparation
                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));
                // execution
                ps.executeUpdate();
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // we treat the 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] محتويات الجدول:


// product list
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            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)));
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(rs, ps, connexion);
        }
    }

السطر 14، عبارة SQL المعدة هي كما يلي:


public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";

طريقة [display] (السطر 18) هي كما يلي:


    // display jSON of an object
    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] بتحديث منتجات معينة:


    // product updates
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // table is updated
            ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
            // category 1
            ps.setInt(1, 1);
            // execution
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // we treat the 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] بإدراج منتجين بنفس المفتاح الأساسي في الجدول، وهو أمر غير ممكن. وبما أننا في معاملة، فسيتم التراجع عن الإدراج الأول.


    // add 2 products with the same primary keys
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // add 1 line
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
            // execution
            ps.executeUpdate();
            // we add the same line a 2nd time, with the same primary key
            // the INSERTion must fail and neither element must be inserted because of the transaction
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
                    connexion, e1);
        } finally {
            // we treat the 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]:

Image

سنستخدم مصدر بيانات تم تنفيذه بواسطة فئة [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 /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- configuration JDBC of 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] هو الذي يتضمن في تبعياته في Maven المكتبة التي توفر تنفيذًا لمصدر بيانات [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 {
    // data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        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 {
 
    // mapper jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
    // data source
    private static DataSource dataSource;
 
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = null;
        try {
            // spring context retrieval
            ctx = new AnnotationConfigApplicationContext(AppConfig.class);
            // data source recovery
            dataSource = ctx.getBean(DataSource.class);
            // empty table [PRODUITS]
            System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
            delete();
...
        // finish
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }
 
    // product list
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            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)));
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // we treat the 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 /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- configuration JDBC of 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]. وعلى وجه الخصوص، تستخدم تبعية Maven لمشروع [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 {
 
    // add products
    public List<Produit> addProduits(List<Produit> produits);
 
    // list of all products
    public List<Produit> getAllProduits();

    // a special product
    public Produit getProduitById(int id);
 
    public Produit getProduitByName(String name);
 
    // several product updates
    public int updateProduits(List<Produit> produits);
 
    // removal of all products
    public int deleteAllProduits();
 
    // removal of several products
    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;
 
    // manufacturers
    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 {
    // data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        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 {
 
    // add products
    public List<Produit> addProduits(List<Produit> produits);
 
    // list of all products
    public List<Produit> getAllProduits();
 
    // a special product
    public Produit getProduitById(int id);
 
    public Produit getProduitByName(String name);
 
    // several product updates
    public int updateProduits(List<Produit> produits);
 
    // removal of all products
    public int deleteAllProduits();
 
    // removal of several products
    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 {
 
    // class name
    private String simpleClassName = getClass().getSimpleName();
    // data source
    @Autowired
    protected DataSource dataSource;
 
    // manufacturer
    public Dao1() {
        System.out.println("building Dao1...");
    }
 
    // ------------------------------- interface
    @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) {
...
    }
 
    // ---------------------------------------- local methods
    // management finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        ...
    }
 
    // catch management
    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 الخاصة بالطرق المختلفة على النحو التالي:


    // gestion catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
        // annulation transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            e2.printStackTrace();
        }
        // daoException
        return new DaoException(code, th, simpleClassName);
}
  • السطر 2: تم تعريف الطريقة على أنها [protected]، مما يسمح للفئات الفرعية باستخدامها دون أن تكون عامة. وهي تأخذ المعلمات التالية:
    • [Connection connection]: الاتصال بنظام إدارة قواعد البيانات (DBMS) — قد يكون null؛
    • [Throwable th]: الاستثناء الذي حدث وسيتم تغليفه في نوع [DaoException
    • [int code]: رمز خطأ يُستخدم إذا أنشأت الطريقة استثناءً جديدًا من نوع [DaoException
  • الأسطر 4–7: الدور الأساسي لهذه الطريقة هو التراجع عن المعاملة المرتبطة بالاتصال الذي تم تمريره كمعلمة 1؛
  • الأسطر 8–10: إذا فشل التراجع عن المعاملة، يتم كتابة تتبع الاستثناء إلى وحدة التحكم. لا يوجد الكثير مما يمكننا فعله لأننا سنقوم بإلقاء استثناء في السطر 12؛

يتم التعامل مع كتل *finally* الخاصة بالطرق المختلفة على النحو التالي:


// management finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        // closure ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {
 
            }
        }
        // closure [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {
 
            }
        }
        // close connection
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // record the error if possible
                if (daoException == null) {
                    daoException = new DaoException(code, e3, simpleClassName);
                }
            }
        }
        // result
        return daoException;
    }
  • السطر 2: تم إعلان هذه الطريقة أيضًا على أنها [protected]. وهي تأخذ المعلمات التالية:
    • [ResultSet rs]: مجموعة النتائج [ResultSet] في حالة تنفيذ عملية [SELECT] — قد تكون فارغة؛
    • [PreparedStatement ps]: [PreparedStatement] الذي تم تنفيذه — قد يكون فارغًا؛
    • [Connection connection]: الاتصال بنظام إدارة قواعد البيانات — قد يكون فارغًا؛
    • [int code]: رمز خطأ لاستخدامه إذا أطلقت الطريقة استثناء [DaoException] جديد؛
    • [DaoException daoException]: [DaoException] في حالة حدوثه قبل كتلة finally — قد يكون فارغًا؛
  • الأسطر 21–30: الغرض الأساسي من هذه الطريقة هو إغلاق الاتصال (السطر 23)؛
  • الأسطر 24–29: إذا حدث استثناء أثناء هذا الإغلاق، فإننا نتحقق من حالة المعلمة [DaoException daoException] التي تم تمريرها إلينا: إذا كان [daoException == null]، فإننا ننشئ [DaoException] جديدًا بالرمز الذي تم تمريره كمعلمة؛
  • السطر 32: يتم إرجاع [DaoException] القديم أو الجديد كنتيجة؛

لن نعرض جميع طرق فئة [Dao]، بل سنعرض بعضها فقط. فهي جميعها متشابهة.

3.6.7.1. طريقة [getProductById]

تُرجع طريقة [getProductById] المنتج الذي يساوي مفتاحه الأساسي المعلمة [id]، أو null في حالة عدم وجود تطابق؛


@Override
    public Produit getProduitById(int id) {
        // connection resources
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        // initially no exceptions
        DaoException daoException = null;
        // the product you are looking for
        Produit produit = null;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            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));
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 112);
        } finally {
            // we treat the finally
            daoException = doFinally(rs, ps, connexion, 113, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return produit;
    }
  • السطر 10: تم تعيين المنتج المراد إرجاعه على null؛
  • السطر 19: عبارة SQL [ConfigJdbc.V2_SELECT_PRODUCT_BYID] هي كما يلي:

public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";

  • الأسطر 22–24: إذا كان [ResultSet] يحتوي على صف، يتم استخدامه لإنشاء المنتج المراد إرجاعه؛ وإلا، يظل المنتج المراد إرجاعه فارغًا؛
  • السطر 41: يتم إرجاع المنتج؛
  • السطر 8: يتم تهيئة [DaoException] الخاصة بالطريقة إلى null؛
  • السطر 31: تقوم طريقة [doCatchException] بإنشاء [DaoException
  • السطر 34: المعلمة [daoException] للطريقة [doFinally] إما أن تكون null أو الاستثناء الذي أنشأته الطريقة [doCatchException]. الطريقة [doFinally]:
    • تترك هذه المعلمة دون تغيير إذا نجحت في إغلاق الاتصال؛
    • تترك هذه المعلمة دون تغيير إذا فشلت في إغلاق الاتصال وحدثت [DaoException] مسبقًا؛
    • تنشئ [DaoException] جديدة إذا فشلت في إغلاق الاتصال ولم تحدث [DaoException] من قبل؛
  • الأسطر 37-39: إذا كانت [daoException] المحلية غير فارغة، يتم إلقاء الاستثناء؛ وإلا، يتم إرجاع النتيجة المطلوبة (السطر 41)؛

3.6.7.2. طريقة [deleteProducts]

تقوم طريقة [deleteProduits] بحذف المنتجات التي تم تمرير مفاتيحها الأساسية إليها كمعلمات. وتُرجع عدد المنتجات المحذوفة.


@Override
    public int deleteProduits(int[] ids) {
        // connection resources
        PreparedStatement ps = null;
        Connection connexion = null;
        // initially no exceptions
        DaoException daoException = null;
        // number of products updated
        int nbProduits = 0;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // we do away with products
            ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
            for (int id : ids) {
                // settings
                ps.setInt(1, id);
                // execution
                nbProduits += ps.executeUpdate();
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 171);
        } finally {
            // we treat the finally
            daoException = doFinally(null, ps, connexion, 172, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        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. طريقة [updateProducts]

تقوم طريقة [updateProduits] بتحديث المنتجات التي تم تمريرها إليها كمعلمات في قاعدة البيانات. وتُرجع عدد المنتجات التي تم تحديثها.


@Override
    public int updateProduits(List<Produit> produits) {
        // connection resources
        PreparedStatement ps = null;
        Connection connexion = null;
        // initially no exceptions
        DaoException daoException = null;
        // number of products updated
        int nbProduits = 0;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // table [PRODUITS] is updated
            ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
            for (Produit produit : produits) {
                // settings
                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());
                // execution
                nbProduits += ps.executeUpdate();
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 131);
        } finally {
            // we treat the finally
            daoException = doFinally(null, ps, connexion, 132, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        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. طريقة [addProducts]

تقوم طريقة [addProducts] بإضافة المنتجات التي تم تمريرها إليها كمعلمات إلى قاعدة البيانات. وتقوم بإرجاع هذه المنتجات نفسها مع مفاتيحها الأساسية (قبل إضافتها إلى قاعدة البيانات، لا تحتوي المنتجات على مفتاح أساسي).


@Override
    public List<Produit> addProduits(List<Produit> produits) {
        // connection resources
        PreparedStatement ps = null;
        Connection connexion = null;
        // initially no exceptions
        DaoException daoException = null;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // in read/write mode
            connexion.setReadOnly(false);
            // start of transaction
            connexion.setAutoCommit(false);
            // add elements to table [PRODUITS]
            String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
            ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
            for (Produit produit : produits) {
                // settings
                ps.setString(1, produit.getNom());
                ps.setLong(2, produit.getCategorie());
                ps.setDouble(3, produit.getPrix());
                ps.setString(4, produit.getDescription());
                // order execution
                ps.executeUpdate();
                // generated primary key
                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()));
                }
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException | RuntimeException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 151);
        } finally {
            // we treat the finally
            daoException = doFinally(null, ps, connexion, 152, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        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]، فإن نظام إدارة قواعد البيانات (DBMS) سيقوم بإنشاء مفتاح أساسي لكل عملية إدراج. وتكمن المشكلة في استرداد هذا المفتاح. وهذه نقطة مهمة لأن العمليات على المنتجات تتم عبر مفاتيحها الأساسية. ولذلك، نحتاج إلى معرفة هذه المفاتيح؛

  • الأسطر 17-33: حلقة إدراج المنتج؛
  • السطر 16: شكل محدد من طريقة [prepareStatement]. المعلمة الثانية [generatedColumns] هي مصفوفة من أسماء الأعمدة التي نريد استرداد قيمها بعد الإدراج. في السطر 16، حددنا أننا نريد استرداد قيمة عمود [id]. لاحظ هنا أنه على الرغم من أن أسماء أعمدة الجدول لا تميز بين الأحرف الكبيرة والصغيرة، فإن نظام إدارة قواعد البيانات PostgreSQL يتطلب أن يكون هذا الاسم بأحرف صغيرة. هذا هو عادةً نوع المشكلة التي نواجهها عند نقل الكود من نظام إدارة قواعد بيانات إلى آخر؛
  • السطر 24: إدراج صف في قاعدة البيانات؛
  • السطر 26: استرداد قائمة القيم من الأعمدة المحددة في السطر 16 إلى [ResultSet]. هنا، بالنسبة لإدراج واحد، سيحتوي [ResultSet] على صف واحد، وسيحتوي هذا الصف على عمود واحد يحتوي على المفتاح الأساسي؛
  • السطر 28: استرداد المفتاح الأساسي الذي تم إنشاؤه بواسطة نظام إدارة قواعد البيانات؛
  • الأسطر 29-32: إذا لم يتم الحصول على المفتاح الأساسي الذي تم إنشاؤه، يتم إلقاء [RuntimeException]، والتي سيتم تغليفها في [DaoException] (الأسطر 38-40)؛

3.6.8. فئة [Dao2]

  

فئة [Dao2] هي نسخة معدلة من فئة [Dao1] تستخدم صيغة تسمى try-with-resource(resource):

1
2
3
4
try(resource){
...
}
...
  • [resource] هو مورد يُنفِّذ واجهة [java.lang.AutoCloseable]. تندرج جميع الموارد التي يتم تحريرها باستخدام طريقة [close] ضمن هذه الفئة. تضمن هذه الصيغة إغلاق [resource] في السطر 4. وهذا يغني عن الحاجة إلى كتابة جملة [finally] لتنفيذ عملية الإغلاق هذه؛

لنأخذ طريقة [getAllProducts] لفئة [Dao2] كمثال:


    @Override
    public List<Produit> getAllProduits() {
        // possible exception
        DaoException daoException = null;
        // product list
        List<Produit> produits = new ArrayList<Produit>();
        try (Connection connexion = dataSource.getConnection()) {
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            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)));
                    }
                }
                // end transaction
                connexion.commit();
                // return to default mode
                connexion.setAutoCommit(true);
            } catch (SQLException e1) {
                // cancel the transaction
                daoException = doRollback(connexion, e1, 111);
            }
        } catch (SQLException e2) {
            // we handle the exception
            if (daoException == null) {
                daoException = new DaoException(112, e2, simpleClassName);
            }
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return produits;
}
  • السطر 7: كتلة try مع المورد [Connection]. يضمن السطر 27 إغلاقه؛
  • السطر 13: كتلة try مع المورد [PreparedStatement]. يضمن السطر 23 إغلاقه؛
  • السطر 14: كتلة try مع المورد [ResultSet]. يضمن السطر 19 إغلاقه؛
  • السطر 25: يتم التراجع عن المعاملة على النحو التالي:

    private DaoException doRollback(Connection connexion, Throwable e1, int code) {
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // génération de l'exception
        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 {
 
    // layer [DAO]
    @Autowired
    @Qualifier("dao1")
    private IDao dao;
 
    @Override
    IDao getDao() {
        return dao;
    }
 
}
  • تمت مناقشة التعليقات التوضيحية في السطرين 12-13 في القسم 2.5.5. وهي تسمح لاختبار JUnit بالوصول بسهولة إلى سياق Spring وبياناته. يتم تكوين هذا السياق بواسطة فئة [AppConfig] (السطر 12) التي تمت مناقشتها في القسم 2.4.
  • السطر 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 {
 
    // layer [DAO]
    abstract IDao getDao();
 
    // mapper jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
 
    @Before
    public void clean() {
        // the base is cleaned before each test
        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() {
    ....
    }

    // -------------- private methods
...
}
  • السطر 19: فئة [AbstractJUnitTestDao] هي فئة مجردة؛
  • السطر 22: الطريقة المجردة [getDao]، التي توفر مرجعًا لطبقة [DAO] المراد اختبارها. يتم تنفيذ هذه الطريقة بواسطة الفئات الفرعية؛
  • السطر 25: أداة تعيين JSON تسمح لنا بعرض قيمة JSON للمنتجات على وحدة التحكم؛
  • الأسطر 27–32: قبل كل اختبار (السطر 27)، يتم مسح جدول [PRODUCTS

3.6.9.2. الطريقة الخاصة [fill]

تُستخدم الطريقة الخاصة [fill] لإضافة المنتجات إلى جدول [PRODUCTS].


private List<Produit> fill(int nbProduits) {
        log("Remplissage de la base de données", 1);
        // on crée une liste de produits
        List<Produit> produits = new ArrayList<Produit>();
        for (int i = 0; i < nbProduits; i++) {
            int n = i + 1;
            // int id, String nom, int categorie, double prix, String description
            produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
                    "DESC%s", n)));
        }
        // on la persiste en base - on récupère des produits avec leur clé primaire
        produits = getDao().addProduits(produits);
        // on crée un dictionnaire des produits pour pouvoir les retrouver + facilement
        // la clé du dictionnaire est la clé primaire du produit en base
        for (Produit produit : produits) {
            mapProduits.put(produit.getId(), produit);
        }
        // on rend les produits
        return produits;
    }
  • السطر 1: تقوم طريقة [fill] بإدراج [nbProducts] في جدول [PRODUCTS]، الذي يُفترض أنه فارغ؛
  • الأسطر 3–10: إنشاء قائمة بالمنتجات بالشكل التالي:

new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format("DESC%s", n)));

التي تستخدم منشئ Product(int id, String name, int category, double price, String description). قيمة المعلمة الأولى [id] (المفتاح الأساسي لجدول [PRODUCTS]) غير ذات صلة لأن طريقة [addProducts] في السطر 10 لا تدخلها في قاعدة البيانات وتسمح لنظام إدارة قواعد البيانات (DBMS) بتوليد قيمتها؛

  • السطر 12: يتم حفظ قائمة المنتجات في قاعدة البيانات. يتم تعيين مفتاح أساسي جديد [id] لكل منتج في هذه القائمة. تعرض طريقة [addProduits] معلمتها [produits]. لذلك كان بإمكاننا تجاهل استرداد النتيجة؛
  • الأسطر 15-17: يتم وضع المنتجات في قاموس:

    // dictionnaire des produits
    private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();

مفتاح القاموس هو المفتاح الأساسي للمنتج، والقيمة المرتبطة به هي المنتج نفسه؛

  • السطر 19: نُرجع قائمة المنتجات؛

3.6.9.3. اختبار [getProducts]

وهو كما يلي:


    @Test
    public void getProduits() throws JsonProcessingException {
        // remplissage
        fill(10);
        // liste des produits
        log("Liste des produits", 2);
        List<Produit> produits = getDao().getAllProduits();
        affiche(produits);
        // on vérifie que la liste récupérée et celle persistée sont les mêmes
        for (Produit produit : produits) {
            Produit found = mapProduits.get(produit.getId());
            Assert.assertEquals(found, produit);
            mapProduits.remove(found.getId());
        }
        // tous les produits initiaux doivent avoir disparu du dictionnaire
        Assert.assertEquals(0, mapProduits.size());
}
}
  • السطر 4: تمت إضافة 10 منتجات إلى قاعدة البيانات؛
  • السطر 7: بمجرد الانتهاء من ذلك، نطلب عرض جميع المنتجات الموجودة في قاعدة البيانات؛
  • السطر 8: نعرضها. الهدف هو التحقق من أن المنتجات قد تم حفظها بنجاح وأن لها مفتاحًا أساسيًا؛
  • الأسطر 10–13: نتحقق من أن المنتجات المسترجعة مطابقة لتلك التي قمنا بحفظها وأنها موجودة في القاموس [mapProduits
  • السطر 11: نسترد من القاموس المنتج الذي يحمل نفس المفتاح الأساسي الذي تم إرجاعه من قاعدة البيانات. وهذا يوضح أن المنتجات التي تم حفظها قد تم بالفعل تعيين مفتاح أساسي لها؛
  • السطر 12: نتأكد من أن المنتجين متطابقان. تذكر أن فئة [Product] قد عرّفت طريقة [equals] (انظر القسم 3.3.4
  • السطر 13: يتم إزالة العنصر الذي تم العثور عليه من القاموس؛
  • السطر 16: نتحقق من أن قاموس المنتجات الأولية فارغ بالفعل، مما يعني أن هذه المنتجات الأولية كانت موجودة جميعها في قائمة المنتجات التي تم استردادها من قاعدة البيانات؛

طريقة [display] في السطر 8 هي الطريقة الخاصة التالية:


    // product list display
    private <T> void affiche(List<T> elements) throws JsonProcessingException {
        for (T element : elements) {
            System.out.println(jsonMapper.writeValueAsString(element));
        }
}
  • السطر 2: طريقة [display] هي طريقة عامة. يتم تحديد معلماتها بواسطة نوع T، ويُشار إليها نحويًا بـ <T>. إذا تم تحديد معلماتها بواسطة نوعين T1 و T2، فسنكتب <T1,T2>. صيغة الطريقة m التي يتم تحديد معلماتها بواسطة نوع T هي كما يلي:
portée <T> type_résultat m(... , T value1, ...){
...
    T value2=...
}

في كود الطريقة m، سنجد بيانات من النوع T. يمكن بعد ذلك استدعاء الطريقة m لمثيل c من الفئة C على النحو التالي:

type_résultat r=c.<T1>m(..., T1 value1, ..) ;

حيث T1 هو النوع الفعلي الذي سيحل محل النوع الشكلي T للطريقة m. في معظم الأحيان، يستطيع المُترجم استنتاج النوع T1 من معلمات الطريقة m. لذلك، غالبًا ما يتم تبسيط العبارة السابقة إلى:

type_résultat r=c.m(..., T1 value1, ..) ;

لنعد إلى الطريقة [display]. تعرض هذه الطريقة قائمة بالعناصر من النوع T. وهذا ممكن لأن أداة تعيين JSON المستخدمة في السطر 4 قادرة على عرض تمثيل JSON لأي نوع كائن. في هذا المثال المحدد، سيكون النوع T الوحيد المستخدم هو النوع [Product].

كان من الممكن أيضًا كتابة طريقة [display] على النحو التالي:


    // product list display
    private void affiche(Object o) throws JsonProcessingException {
            System.out.println(jsonMapper.writeValueAsString(o));
        }

نظرًا لأن المعلمة الفعلية عبارة عن قائمة من المنتجات، فإن السطر 3 كان سيطبع تمثيل JSON لتلك القائمة. وهذا يختلف عن طباعة تمثيل كل عنصر من عناصرها واحدًا تلو الآخر.

الناتج الناتج عن اختبار [getProducts] هو كما يلي:

-- Liste des produits
{"id":150189,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":150190,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":150191,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":150192,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":150193,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":150194,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":150195,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":150196,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":150197,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":150198,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}

3.6.9.4. اختبار [getProductBy]

وهو كما يلي:


    @Test
    public void getProduitBy() {
        // remplissage
        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: تُستخدم طريقة [getProductByName] الخاصة بواجهة [IDao] لاسترداد المنتج المسمى [NAME3
  • السطر 7: ثم تُستخدم طريقة [getProductById] الخاصة بواجهة [IDao] لاسترداد المنتج نفسه، الذي يتم تحديده هذه المرة بواسطة مفتاحه الأساسي؛
  • الأسطر 8-10: نتحقق من أن [product2] و [product] لهما نفس الخصائص؛

3.6.9.5. اختبار [doInsertsInTransaction]

وهو كما يلي:


    @Test
    public void doInsertsInTransaction() {
        log("Ajout de deux produits de même nom", 1);
        // on fait l'insertion
        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;
        }
        // vérifications
        Assert.assertTrue(erreur);
        List<Produit> produits = getDao().getAllProduits();
        Assert.assertEquals(0, produits.size());
}
  • الأسطر 5-7: نقوم بإنشاء قائمة من منتجين يحملان نفس الاسم [x]؛
  • السطر 10: يتم إدراج هذين المنتجين في جدول [PRODUCTS]، وهو فارغ (طريقة [clean] المُعلَّمة بـ [@Before]). سينجح الإدراج الأول، ولكن لن ينجح الثاني، لأن جدول [PRODUCTS] يحتوي على قيد فريد على أسماء المنتجات. لذلك يجب أن تحدث استثناء. يتم اختبار ذلك في السطر 15؛
  • نظرًا لأن جميع طرق واجهة [IDao] يتم تنفيذها ضمن معاملة، فإن فشل الإدراج الثاني سيؤدي إلى التراجع عن المعاملة بأكملها، بما في ذلك الإدراج الأول. وفي النهاية، لن يتم إجراء أي إدراجات في جدول [PRODUCTS
  • السطران 16-17: نتحقق من ذلك عن طريق استرداد قائمة المنتجات في جدول [PRODUCTS] والتأكد من أن هذه القائمة فارغة؛

3.6.9.6. اختبار [updateProducts]

وهو كما يلي:


    @Test
    public void updateProduits() {
        // remplissage
        fill(10);
        log("Mise à jour du prix des produits de catégorie 1", 1);
        // on récupère les produits
        List<Produit> produits = getDao().getAllProduits();
        // on met à jour ceux de catégorie 1
        List<Produit> updated = new ArrayList<Produit>();
        int nbUpdated = 0;
        for (Produit produit : produits) {
            if (produit.getCategorie() == 1) {
                // int id, String nom, int categorie, double prix, String description
                updated
                        .add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
                nbUpdated++;
            }
        }
        int nbProduits = getDao().updateProduits(updated);
        // vérifications
        // Assert.assertEquals(nbUpdated, nbProduits); -- does not work with 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: استرجاع عدد المنتجات التي تم تحديثها بواسطة عملية [updateProducts
  • السطر 21: نتحقق من أن هذا الرقم هو بالفعل الرقم المتوقع. ينجح هذا الاختبار لجميع أنظمة إدارة قواعد البيانات باستثناء DB2. لذلك قمنا بتعليقه؛

3.6.9.7. اختبار [deleteProducts]

هذا الاختبار كما يلي:


    @Test
    public void deleteProduits() {
        // filling
        fill(10);
        log("deleteProduits", 1);
        // product list
        List<Produit> produits = getDao().getAllProduits();
        // discontinuation of two products
        Produit produit0 = produits.get(0);
        Produit produit5 = produits.get(5);
        int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
        // checks
        // Assert.assertEquals(2, nbDeleted); -- does not pass with 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 مع نظام إدارة قواعد البيانات DB2. ويجتاز الاختبار مع أنظمة إدارة قواعد البيانات الأخرى؛

3.6.9.8. اختبارات الأداء

أدرجنا ثلاث طرق في الاختبارات الغرض الوحيد منها هو تقييم أداء نظام إدارة قواعد البيانات:


    @Test
    public void perf1() {
        // remplissage
        fill(10000);
    }

    @Test
    public void perf2() {
        // remplissage
        fill(10000);
        // modification
        List<Produit> produits = getDao().getAllProduits();
        // on met à jour ceux de catégorie 1
        List<Produit> updated = new ArrayList<Produit>();
        for (Produit produit : produits) {
            // int id, String nom, int categorie, double prix, String description
            updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
        }
        getDao().updateProduits(updated);
    }
 
    @Test
    public void perf3() {
        // remplissage
        fill(10000);
        // suppression
        List<Produit> produits = getDao().getAllProduits();
        // clés primaires
        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: إدراج 10,000 منتج؛
  • الأسطر 8–20: إدراج 10,000 منتج ثم تعديلها باستخدام مفاتيحها الأساسية؛
  • الأسطر 23-34: إدراج 10,000 منتج، ثم حذفها باستخدام مفاتيحها الأساسية؛

لتشغيل اختباري [JUnitTestDao1] و [JUnitTestDao2]، يمكن استخدام تكوينات الاختبار التالية:

فيما يلي نتائج اختبار [JUnitTestDao1]:

في [1] نتائج [JUnitTestDao1] وفي [2] نتائج [JUnitTestDao2]. لا توجد فروق ذات دلالة إحصائية بينهما. في [1]:

  • اجتاز الاختبار؛
  • يستغرق إدخال 10,000 منتج 3.15 ثانية؛
  • يستغرق إدخال 10,000 منتج ثم تعديلها 4.80 ثانية؛
  • يستغرق إدخال 10,000 منتج ثم حذفها 4.40 ثانية؛
  • لذا فإن العملية الأكثر تكلفة هي الإدراج؛