Skip to content

6. [الدورة]: مقدمة إلى واجهة برمجة تطبيقات JDBC

الكلمات المفتاحية: قواعد البيانات العلائقية، واجهة برمجة تطبيقات JDBC، استثناء SQLException.

6.1. الدعم

يحتوي المجلد [support / chap-06] على مشاريع Eclipse الخاصة بهذا الفصل.

6.2. البنية

طبقة JDBC (Java Database Connectivity) هي واجهة عالمية للوصول إلى قواعد البيانات. وهي تقدم دائمًا نفس الواجهة لطبقة [DAO]. إذا قمت بتغيير نظام إدارة قواعد البيانات (DBMS)، فما عليك سوى تغيير برنامج تشغيل JDBC. وتبقى طبقة [DAO] دون تغيير.

6.3. خطوات تشغيل قاعدة البيانات

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

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

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

6.3.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 قد تم تضمينه في مسار فئات المشروع.

6.3.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] بحيث يتم إغلاقه بغض النظر عما إذا حدث استثناء أم لا.

6.3.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] للأعمدة [PRODUCTS.NAME] و [CATEGORIES.NAME] على نظام إدارة قواعد البيانات (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) تحرير الموارد المخصصة لها؛

6.3.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: يتم تنفيذ المعاملة؛

6.3.5. الخطوة 4 - إغلاق الاتصال

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

6.4. مشروع نموذجي

6.4.1. الدعم

يحتوي المجلد [support / chap5] على مشاريع Eclipse الخاصة بهذا الفصل [1، 2]. ويحتوي المجلد [database] على البرنامج النصي SQL لإنشاء قاعدة بيانات MySQL النموذجية الخاصة بهذا الفصل [1، 3].

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

تستخدم الأمثلة التالية قاعدة بيانات MySQL التالية:

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

سنقوم بإنشائه باستخدام أداة [WampServer] على النحو التالي [1-9]:

6.4.3. مشروع Eclipse

  

المشروع هو مشروع 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>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
    </dependencies>
</project>
  • الأسطر 8–12: برنامج تشغيل JDBC لنظام إدارة قواعد البيانات MySQL5؛
  • الأسطر 13–17: مكتبة قادرة على معالجة JSON (ترميز كائنات JavaScript) (انظر القسم 22.6). سنستخدمها لعرض المنتجات من قاعدة البيانات بتنسيق JSON؛

6.4.4. فئة Product

فئة [Product] هي كما يلي:


package istia.st.jdbc;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
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
    ...
 
    // to String
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}
  • السطر 34: نستخدم مكتبة JSON لعرض سلسلة JSON الخاصة بالمنتج. ينتج عن ذلك مخرجات مشابهة لما يلي:
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"}

تتمثل ميزة طريقة [toString] المذكورة أعلاه في أنه حتى في حالة إضافة حقول إلى الفئة أو حذفها منها، تظل طريقة [toString] سارية المفعول. علاوة على ذلك، إذا كانت الحقول نفسها كائنات (قوائم، مصفوفات، قواميس، كائنات مستخدم)، يمكن لمكتبات JSON بدورها تحويلها إلى سلاسل JSON؛

6.4.5. فئة [Static]

تجمع فئة [Static] الطرق التي تحتوي على كود يُستخدم بشكل متكرر في الفئة الرئيسية:


package istia.st.jdbc;
 
import java.util.ArrayList;
import java.util.List;
 
public class Static {
 
    public 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;
    }
 
    public 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));
        }
    }
}
  • الأسطر 8–19: تُرجع قائمة بالأخطاء مغلفة في كائن من النوع [Throwable]، وهو الفئة الفائقة لفئة [Exception
  • الأسطر 21-28: تعرض قائمة بالرسائل على الشاشة؛

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

6.4.6. هيكل الفئة الرئيسية


package istia.st.jdbc;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
public class IntroJdbc01 {
 
    // constants
    final static String url = "jdbc:mysql://localhost:3306/dbIntroJdbc";
    final static String user = "root";
    final static String passwd = "";
    final static String insert = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    final static String delete = "DELETE FROM PRODUITS";
    final static String select = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    final static String update = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    final static String insert2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
 
    public static void main(String[] args) {
        // loading the JDBC driver from MySQL
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // empty table [PRODUITS]
        delete();
        // fill it
        insert();
        // we read it
        select();
        // update
        update();
        // display
        select();
        // insertion of two identical elements
        // insertion must fail and neither element is inserted because of the transaction
        insert2();
        // we check
        select();
        // finish
        System.out.println("Travail terminé");
    }
 
    // product list
    private static void select() {
...
    }
 
    // product deletion
    public static void delete() {
...
    }
 
    // add products
    public static void insert() {
...
    }
 
    // add 2 products
    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) {
 
            }
        }
        // close connection
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // display error msg
                Static.show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
                        Static.getErreursFromThrowable(e3));
            }
        }
    }
 
    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(title, Static.getErreursFromThrowable(e2));
        }
    }
}

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

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


// product deletion
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // empty table [PRODUITS]
            ps = connexion.prepareStatement(delete);
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } 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, null, connexion);
        }
    }

يستخدم هذا المثال المعاملات. تسمح لك المعاملة بتجميع عبارات SQL التي يجب أن تنجح جميعها أو يتم التراجع عنها جميعًا. هناك أربع عمليات يجب الانتباه إليها:

  • بدء معاملة: [connection.setAutoCommit(false)];
  • إنهاء المعاملة بنجاح: [connection.commit()]. في هذه الحالة، يتم تثبيت جميع العمليات التي تم إجراؤها على قاعدة البيانات أثناء المعاملة؛
  • إنهاء المعاملة بفشل: [connection.rollback()]. في هذه الحالة، يتم التراجع عن جميع العمليات التي تم إجراؤها على قاعدة البيانات أثناء المعاملة؛
  • العودة إلى وضع [auto-commit]، وهو الوضع الافتراضي لواجهة برمجة تطبيقات JDBC: [connection.setAutoCommit(true)]. في هذا الوضع، يكون كل جملة SQL جزءًا من معاملة. وبالتالي، إذا قمت بإجراء عمليتي إدراج وفشلت الثانية:
    • في وضع [AutoCommit=true]، يبقى الإدراج الأول (تم تنفيذه بواسطة أول AutoCommit
    • في وضع [AutoCommit=false]، يتم التراجع عن عملية الإدراج الأولى؛

في أمثلةنا، كلما حدث استثناء، نقوم بإلغاء المعاملة في طريقة [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));
        }
}

6.4.8. إنشاء محتويات جدول المنتجات

تقوم طريقة [insert] بإنشاء محتوى الجدول:


// add products
    public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // fill the table
            ps = connexion.prepareStatement(insert);
            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();
            // return to default mode
            connexion.setAutoCommit(true);
        } 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, null, connexion);
        }
    }

6.4.9. عرض محتويات جدول المنتجات

تعرض طريقة [select] محتويات الجدول:


    // product list
    private static void 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);
            // table [PRODUITS] is read
            ps = connexion.prepareStatement(select);
            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();
            // return to default mode
            connexion.setAutoCommit(true);
        } 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(null, null, connexion);
        }
}

6.4.10. تحديث محتويات الجدول

تقوم طريقة [update] بتحديث منتجات معينة:


// product updates
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // table is updated
            ps = connexion.prepareStatement(update);
            // category 1
            ps.setInt(1, 1);
            // execution
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } 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, null, connexion);
        }
    }

6.4.11. دور المعاملة

تقوم الطريقة [insert2] بإدراج منتجين لهما نفس المفتاح الأساسي في الجدول، وهو أمر غير مسموح به. وبما أننا في معاملة، فسيتم التراجع عن الإدراج الأول.


// add 2 pro ducts with the same primary keys
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // add 1 line
            ps = connexion.prepareStatement(insert2);
            // execution
            ps.executeUpdate();
            // we add the same line a 2nd time, with the same primary key
            // the insertion must fail and neither of the two elements must be inserted because of the transaction
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }

6.4.12. النتائج

فيما يلي نتائج تنفيذ الطريقة [main]:

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"}
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"}
Les erreurs suivantes se sont produites lors de l'ajout : 
- Duplicate entry '100' for key 'PRIMARY'
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"}
Travail terminé

6.5. استخدام مصدر بيانات [DataSource]

سنعيد النظر في التطبيق السابق باستخدام مصدر بيانات [javax.sql.DataSource]:

Image

سنستخدم مصدر بيانات تم تنفيذه بواسطة فئة [org.apache.tomcat.jdbc.pool.DataSource]. تستخدم هذه الفئة تجمع اتصالات، أي مجموعة من الاتصالات المفتوحة:

  • عندما يتم إنشاء مثيل للمجموعة، يتم فتح عدد معين من الاتصالات بالقاعدة البيانات. هذا العدد قابل للتكوين؛
  • عندما يفتح كود Java اتصالاً، يتم توفيره من المجمع؛
  • عندما يغلق كود Java اتصالاً، يتم إرجاعه إلى المجمع؛

في النهاية، يتم فتح الاتصالات مرة واحدة فقط، مما يحسن أداء الوصول إلى قاعدة البيانات. سيتم تعريف مصدر البيانات في فئة تكوين Spring

6.5.1. مشروع Eclipse

  

هذا المشروع هو مشروع 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>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <!-- library jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.3.RELEASE</version>
        </dependency>
        <!-- Tomcat Jdbc -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
            <version>8.0.20</version>
        </dependency>
    </dependencies>
</project>
  • الأسطر 21–25: التبعية على Spring؛
  • الأسطر 27–31: التبعية للمكتبة التي توفر مصدر البيانات؛

فيما يلي فئة التكوين Spring [AppConfig]:


package istia.st.jdbc;
 
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class AppConfig {
 
    // constantes
    final static String URL = "jdbc:mysql://localhost:3306/dbIntroJdbc";
    final static String USER = "root";
    final static String PASSWD = "";
    final static String INSERT = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    final static String DELETE = "DELETE FROM PRODUITS";
    final static String SELECT = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    final static String UPDATE = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    final static String INSERT2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
    final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
 
    @Bean
    public DataSource dataSource() {
        // source de données TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration accès JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // une connexion ouverte initialement
        dataSource.setInitialSize(1);
        // résultat
        return dataSource;
    }
}
  • الأسطر 11–19: تم نقل الثوابت التي تم تعريفها مسبقًا في [IntroJdbc01] إلى [AppConfig
  • الأسطر 31–34: حبة Spring التي تحدد مصدر البيانات؛
  • السطر 24: إنشاء مصدر البيانات، الذي لم يتم تكوينه بعد؛
  • الأسطر 26–29: المعلومات التي تسمح لمصدر البيانات بالاتصال بقاعدة البيانات؛
  • السطر 31: إنشاء تجمع من اتصال واحد. لا نحتاج إلى أكثر من ذلك هنا. لا توجد أبدًا اتصالات متعددة في وقت واحد؛

6.5.2. الفئة الرئيسية

الفئة الرئيسية [IntroJdbc02] هي كما يلي:


package istia.st.jdbc;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
import javax.sql.DataSource;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class IntroJdbc02 {
    // data source
    private static DataSource dataSource;
 
    public static void main(String[] args) {
        // spring context retrieval
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // data source recovery
        dataSource = ctx.getBean(DataSource.class);
        // empty table [PRODUITS]
        delete();
...
        // finish
        ctx.close();
        System.out.println("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);
...
        } 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(null, null, connexion);
        }
    }
...
}
  • السطر 14: مصدر البيانات. لاحظ أنه من النوع [javax.sql.DataSource]، وهو واجهة؛
  • السطر 18: إنشاء مثيلات لكائنات Spring؛
  • السطر 20: الحصول على مرجع لمصدر البيانات. لاحظ أن الفئة الفعلية المستخدمة لم يتم ذكرها مطلقًا. وبالتالي، لا يوجد هنا ما يشير إلى استخدام تطبيق [TomcatJdbc
  • السطر 36: الحصول على اتصال مفتوح؛
  • باقي الكود مطابق لكود الفئة [IntroJdbc01

6.6. الخلاصة

يمكن العثور على مزيد من المعلومات حول إدارة قواعد البيانات في الوثيقة [العمل مع قاعدة بيانات علائقية باستخدام نظام Spring].