Skip to content

6. [Cours]: Вступ до API JDBC

Ключові слова: реляційні бази даних, API JDBC, SQLException.

6.1. Support

Папка [support / chap-06] містить проекти Eclipse з цього розділу.

6.2. Architecture

Рівень JDBC (Java DataBase Connectivity) є універсальним інтерфейсом доступу до баз даних. Він завжди надає однаковий інтерфейс для рівня [DAO]. Якщо змінюється SGBD, достатньо змінити драйвер JDBC. Рівень [DAO] не змінюється.

6.3. Етапи експлуатації бази даних

У наведеній вище архітектурі експлуатація бази даних консольною програмою включає такі етапи:

  1. завантаження драйвера JDBC бази даних;
  2. відкриття з'єднання з базою даних;
  3. виконання команди SQL у базі даних та обробка результатів команди SQL;
  4. закриття з'єднання;

Крок 1 виконується лише один раз. Кроки 2–4 виконуються повторно. Слід зауважити, що з’єднання не залишається відкритим. Його закривають, щойно воно більше не потрібне.

6.3.1. крок 1 — завантаження драйвера JDBC у пам’ять

Код


        // завантаження драйвера JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
             // обробка винятку
}

Операція в рядку 3 має на меті завантажити в пам'ять драйвер JDBC із бази даних. Цю операцію потрібно виконати лише один раз. Однак її повторення не спричиняє помилки. Клас драйвера JDBC шукається в Classpath проекту. Тому в проєкті Eclipse файл [jar], що містить клас драйвера JDBC, має бути включений до Classpath проєкту.

6.3.2. Крок 2 — встановлення з’єднання

Після встановлення драйвера JDBC йому потрібно відкрити з’єднання з BD:

Код


package spring.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class IntroJdbc01 {

...
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // відкриття з'єднання
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // обробка винятку
            ...
        } finally {
         // закриття з'єднання
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // обробити виняток
                ...
            }
         }
}
  • рядки 3–7: класи реалізації інтерфейсу JDBC знаходяться у пакеті [java.sql]. Крім того, у разі помилки всі вони генерують виняток типу [SQLException] (рядки 19, 27). Цей виняток походить від класу [Exception] і є так званим контрольованим винятком: для його обробки необхідно використовувати блок try/catch або, як альтернатива, не обробляти його та вказати, що метод дозволяє винятковій ситуації вийти, доповнивши сигнатуру методу [throws SQLException];
  • рядок 17, [DriverManager.getConnection] — це статичний метод, який очікує три параметри:
    • [url]: URL з бази даних. Це рядок символів, що залежить від використовуваного BD. Для MySQL вона має вигляд [jdbc:mysql://localhost:3306/nom_de_la_bd];
    • [user]: власник з’єднання;
    • [passwd]: його пароль;
  • рядки 24–30: з’єднання має бути закрите у клаузулі [finally], щоб воно закривалося незалежно від того, чи виникло виключення.

6.3.3. крок 3 — видача команд SQL та [SELECT]

Після встановлення з’єднання можна відправляти команди SQL. Спосіб обробки команд читання [SELECT] відрізняється від того, що використовується для операцій оновлення [UPDATE, INSERT, DELETE]. Почнемо з команд SQL та [SELECT]:

Код


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // початок сеансу
            connexion = DriverManager.getConnection(url, user, passwd);
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі «тільки для читання»
            connexion.setReadOnly(true);
            // зчитування таблиці [PRODUITS]
            ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // фіксація транзакції
            connexion.commit();
        } catch (SQLException e1) {
            // обробка винятку
             doCatchException(connexion,e1);
        } finally {
            // обробка блоку finally
            doFinally(rs, ps, connexion);
        }

    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • рядки 8, 10: відкриття транзакції (рядок 8) у режимі «тільки для читання» (рядок 10). Транзакція — це послідовність команд SQL, які або всі виконуються успішно, або всі завершуються невдачею. Отже, у транзакції, що містить N команд SQL, якщо команда I+1 завершиться невдало, то попередні I команд будуть скасовані. Для операції читання транзакція не потрібна. Проте створення транзакції «тільки для читання» може дозволити деяким командам SGBD здійснити певні оптимізації;
  • рядок 12: використання [PreparedStatement]. [PreparedStatement] зазвичай має параметри, позначені символом ?. Тут їх немає. [PreparedStatement] — це запит, підготовлений SGBD. Ця підготовка має певну вартість і виконується лише один раз. Потім цей підготовлений запит виконується SGBD із різними фактичними параметрами, які замінять формальні параметри «?». Слід зауважити, що краще вказувати потрібні стовпці, а не використовувати символ * для отримання всіх стовпців. Вказавши назви стовпців, можна потім отримати їхні значення на основі їхнього положення у запиті SELECT;
  • рядок 13: виконання запиту [PreparedStatement]. Отримуємо об’єкт типу [ResultSet];

Об’єкт типу [ResultSet] представляє таблицю, тобто сукупність рядків і стовпців. У певний момент часу доступний лише один рядок таблиці, який називається поточним рядком. Під час початкового створення [ResultSet] поточного рядка немає. Щоб її отримати, потрібно виконати операцію [ResultSet.next()]. Сигнатура методу next така:

    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];
  • стовпець [NOM] буде переміщено до стовпця таблиці [ResultSet] з назвою [myNom];
  • ...

У наведеному вище прикладі ідентифікатори [myCol] називаються мітками стовпців. За відсутності цих міток імена стовпців таблиці [ResultSet] залежать від таблиці SGBD. Коли [SELECT] оперує з однією таблицею, мітками стовпців за замовчуванням будуть імена стовпців, запитувані SELECT. Проблема виникає, коли [SELECT] оперує кількома таблицями, у яких зустрічаються однакові імена стовпців, як у наведеному нижче прикладі:

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

припустимо, що таблиця [PRODUITS] має зовнішній ключ до таблиці [CATEGORIES], що символізується відношенням [Produits].CATEGORIE_ID --> [CATEGORIES].ID, а таблиці [PRODUITS] та [CATEGORIES] обидві мають поле [NOM]. У цьому випадку імена, надані в таблиці [ResultSet] стовпцям [PRODUITS.NOM] та [CATEGORIES.NOM], залежать від таблиці SGBD. Отже, для перенесення даних між файлами SGBD тут слід використовувати мітки стовпців, і ми напишемо:


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

Для обробки різних полів поточного рядка [ResultSet] доступні такі методи:

Type getType("labelColi") 

для отримання стовпця з назвою «labelColi» з поточного рядка, а отже, стовпця з файлу [SELECT], що має цей міток. Type позначає тип поля coli. Можна використовувати такі методи [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Замість імені стовпця можна використовувати його позицію у виконаному запиті [SELECT]:

Type getType(i) 

де i — індекс потрібного стовпця (i>=1).

  • рядки 15–17: отримання значень, зчитаних у BD;
  • рядок 19: транзакція підтверджується (це також називають «фіксацією»). Це завершує її та звільняє ресурси, які SGBD задіяв для неї;
  • рядок 25: ресурси звільняються у [finally]. Ця процедура викликає наступний метод [doFinally]:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // закриття ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // закриття [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // закрити з'єднання
                connexion.close();
            } catch (SQLException e3) {
                 // обробити виняток
            }
        }
    }
  • рядки 3–9: закриття [ResultSet];
  • рядки 11–17: закриття [PreparedStatement];
  • рядки 18–27: закриття з’єднання;

Закриття у рядках 3–17 здаються зайвими, оскільки з’єднання закривається у рядках 18–25. Насправді в деяких випадках це не так, і рекомендується залишити їх [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].

  • рядок 22: виняток обробляється наступним методом [doCatchException]:

    private static void doCatchException(Connection connexion, Throwable th) {
        // скасування транзакції
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // обробити виняток
        }
}
  • рядки 4–6: транзакція скасовується. Це завершує її, і SGBD зможе звільнити ресурси, задіяні для неї;

6.3.4. етап 3 — видача команд SQL та [INSERT, UPDATE, DELETE]

Команди SQL та [INSERT, UPDATE, DELETE] є операціями оновлення: вони змінюють базу даних, але не повертають жодного рядка. Єдина інформація, що повертається, — це кількість рядків, на які вплинула операція оновлення.

Код


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // відкрити з'єднання
            connexion = DriverManager.getConnection(url, user, passwd);
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі читання/запису
            connexion.setReadOnly(false);
            // оновлення таблиці
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // категорія 1
            ps.setInt(1, 10);
            // виконання
            int nbLignes=ps.executeUpdate();
            // фіксація транзакції
            connexion.commit();
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException(connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(null, ps, connexion);
        }
    }
  • рядок 9: з’єднання використовується для читання та запису;
  • рядок 11: операція [PreparedStatement] з 1 параметром (позначеним символом ?). Параметрів може бути кілька. Вони нумеруються, починаючи з 1;
  • рядок 13: єдиному параметру присвоюється значення. Перший параметр [setType] — це позиція параметра в [PreparedStatement] (1, 2, ...), а другий — значення, яке йому присвоюється. Можна використовувати методи [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • рядок 15: використовується метод [executeUpdate], а не [executeQuery], який зарезервовано для команд SELECT. Цей метод повертає кількість рядків, на які вплинула операція. Може дорівнювати 0.
  • рядок 17: транзакція підтверджена;

6.3.5. етап 4 — закриття з'єднання

У багатокористувацькому середовищі з’єднання слід закривати якомога швидше, оскільки SGBD допускає обмежену кількість відкритих з’єднань. У наведених вище прикладах з’єднання закривалося в розділі [finally] операції SQL, щоб воно закривалося незалежно від того, чи стався виняток.

6.4. Приклад проекту

6.4.1. Підтримка

Папка [support / chap5] містить проекти Eclipse з цього розділу [1, 2]. Папка [database] містить скрипт SQL, що дозволяє створити приклад бази даних MySQL, описаний у цьому розділі [1, 3].

6.4.2. Використовувана база даних

У наведених нижче прикладах використовується така база даних MySQL:

 
  • [ID]: первинний ключ у режимі AUTO_INCREMENT (якщо первинний ключ не вказано, SGBD генерує його);
  • [NOM]: назва товару — унікальна;
  • [CATEGORIE]: номер категорії;
  • [PRIX]: його ціна;
  • [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 для SGBD MySQL5;
  • рядки 13–17: бібліотека, здатна обробляти jSON (Javascript Object Notation) (див. параграф 22.6). Ми будемо використовувати її для відображення товарів із бази даних у форматі jSON;

6.4.4. Клас товарів

Клас [Produit] має такий вигляд:


package istia.st.jdbc;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Produit {

    // поля
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;

    // конструктори
    public Produit() {

    }

    public Produit(int id, String nom, int categorie, double prix, String description) {
        this.id = id;
        this.nom = nom;
        this.categorie = categorie;
        this.prix = prix;
        this.description = description;
    }

    // методи getter та setter
    ...

    // в 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) {
        // отримання списку повідомлень про помилки винятку
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // повідомлення про помилку об'єкта, що викликає виняток
            erreurs.add(th.getMessage());
            // перехід до причини об’єкта, що викликає виняток
            th = th.getCause();
        }
        // результат
        return erreurs;
    }
    
    public static void show(String title, List<String> messages){
        // заголовок
        System.out.println(String.format("%s : ",title));
        // повідомлення
        for(String message : messages){
            System.out.println(String.format("- %s",message));
        }
    }
}
  • рядки 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 {

    // константи
    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) {
        // завантаження драйвера JDBC з MySQL
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // очищення таблиці [PRODUITS]
        delete();
        // заповнення таблиці
        insert();
        // зчитування
        select();
        // оновлення
        update();
        // виведення
        select();
        // вставлення двох однакових елементів
        // вставлення має завершитися невдало, і жоден з двох елементів не вставляється через транзакцію
        insert2();
        // перевіряється
        select();
        // завершено
        System.out.println("Travail terminé");
    }

    // список товарів
    private static void select() {
...
    }

    // видалення товарів
    public static void delete() {
...
    }

    // додавання товарів
    public static void insert() {
...
    }

    // додавання 2 товарів
    public static void insert2() {
...
    }

    // оновлення деяких товарів
    public static void update() {
..
    }

    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // закриття ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // закриття [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        // закрити з'єднання
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // відображаються повідомлення про помилки
                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) {
        // відображаються повідомлення про помилки
        Static.show(title, Static.getErreursFromThrowable(th));
        // скасування транзакції
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // відображаються повідомлення про помилки
            Static.show(title, Static.getErreursFromThrowable(e2));
        }
    }
}

6.4.7. Видалення вмісту таблиці товарів

Метод [delete] видаляє вміст таблиці:


// видалення товарів
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // встановлення з'єднання
            connexion = DriverManager.getConnection(url, user, passwd);
            // початок транзакції
            connexion.setAutoCommit(false);
            // очищення таблиці [PRODUITS]
            ps = connexion.prepareStatement(delete);
            ps.executeUpdate();
            // фіксація транзакції
            connexion.commit();
            // повернення до режиму за замовчуванням
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(null, null, connexion);
        }
    }

У цьому прикладі використовуються транзакції. Транзакція дозволяє об’єднати команди SQL, які мають бути виконані всі або скасовані всі. Необхідно знати чотири операції:

  • початок транзакції: [connexion.setAutoCommit(false)];
  • успішне завершення транзакції: [connexion.commit()]. У цьому випадку всі операції, виконані з BD під час транзакції, вважаються дійсними;
  • завершення транзакції з помилкою: [connexion.rollback()]. У цьому випадку всі операції, виконані з BD під час транзакції, скасовуються;
  • повернення до режиму [auto-commit], який є режимом за замовчуванням для API JDBC: [connexion.setAutoCommit(true)]. У цьому режимі кожна команда SQL є предметом транзакції. Отже, якщо виконати дві операції вставки, друга з яких завершилася невдало:
    • у режимі [AutoCommit=true] перша операція вставки зберігається (вона була підтверджена першим AutoCommit);
    • у режимі [AutoCommit=false] перша операція вставки скасовується;

У наших прикладах щоразу, коли виникає виняток, ми скасовуємо транзакцію в методі [doCatchException]:


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // виведення повідомлень про помилки
        Static.show(title, Static.getErreursFromThrowable(th));
        // скасування транзакції
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // виводимо повідомлення про помилки
            Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
        }
}

6.4.8. Створення вмісту таблиці товарів

Метод [insert] створює вміст таблиці:


// додавання товарів
    public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // встановлення з'єднання
            connexion = DriverManager.getConnection(url, user, passwd);
            // початок транзакції
            connexion.setAutoCommit(false);
            // заповнення таблиці
            ps = connexion.prepareStatement(insert);
            for (int i = 0; i < 10; i++) {
                // підготовка
                int n = i + 1;
                ps.setInt(1, n);
                ps.setString(2, String.format("NOM%s", n));
                ps.setInt(3, n / 5 + 1);
                ps.setDouble(4, 100 * (1 + (double) i / 100));
                ps.setString(5, String.format("DESC%s", n));
                // виконання
                ps.executeUpdate();
            }
            // фіксація транзакції
            connexion.commit();
            // повернення до режиму за замовчуванням
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(null, null, connexion);
        }
    }

6.4.9. Виведення вмісту таблиці товарів

Метод [select] відображає вміст таблиці:


    // список товарів
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // відкриття з'єднання
            connexion = DriverManager.getConnection(url, user, passwd);
            // початок транзакції
            connexion.setAutoCommit(false);
            // зчитування таблиці [PRODUITS]
            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)));
            }
            // фіксація транзакції
            connexion.commit();
            // повернення до режиму за замовчуванням
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(null, null, connexion);
        }
}

6.4.10. Оновлення вмісту таблиці

Метод [update] оновлює певні товари:


// оновлення деяких продуктів
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // відкриття з'єднання
            connexion = DriverManager.getConnection(url, user, passwd);
            // початок транзакції
            connexion.setAutoCommit(false);
            // оновлення таблиці
            ps = connexion.prepareStatement(update);
            // категорія 1
            ps.setInt(1, 1);
            // виконання
            ps.executeUpdate();
            // фіксація транзакції
            connexion.commit();
            // повернення до режиму за замовчуванням
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(null, null, connexion);
        }
    }

6.4.11. Роль транзакції

Метод [insert2] вставляє в таблицю два продукти з однаковим первинним ключем, що є неможливим. Оскільки операція виконується в рамках транзакції, перше вставлення буде скасовано.


// додавання  2 товарів з однаковими первинними ключами
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // відкриття з'єднання
            connexion = DriverManager.getConnection(url, user, passwd);
            // початок транзакції
            connexion.setAutoCommit(false);
            // додано 1 рядок
            ps = connexion.prepareStatement(insert2);
            // виконання
            ps.executeUpdate();
            // додається той самий рядок вдруге, тобто з тим самим первинним ключем
            // вставлення має завершитися невдачею, і жоден з цих двох елементів не має бути вставлений через транзакцію
            ps.executeUpdate();
            // фіксація транзакції
            connexion.commit();
            // повернення до режиму за замовчуванням
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout", connexion, e1);
        } finally {
            // обробка блоку 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>
        <!-- бібліотека 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 {

    // константи
    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() {
        // джерело даних TomcatJdbc
        DataSource dataSource = new DataSource();
        // конфігурація доступу JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // спочатку відкрите з'єднання
        dataSource.setInitialSize(1);
        // результат
        return dataSource;
    }
}
  • рядки 11–19: константи, раніше визначені в [IntroJdbc01], перенесені до [AppConfig];
  • рядки 31–34: Spring-бін, що визначає джерело даних;
  • рядок 24: створення джерела даних, яке ще не налаштовано;
  • рядки 26–29: інформація, що дозволяє джерелу даних підключитися до бази даних;
  • рядок 31: створює пул із 1 з’єднання. Більше тут не потрібно. Одночасних з’єднань ніколи не буває;

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 {
    // джерело даних
    private static DataSource dataSource;

    public static void main(String[] args) {
        // отримання контексту Spring
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // отримання джерела даних
        dataSource = ctx.getBean(DataSource.class);
        // очищення таблиці [PRODUITS]
        delete();
...
        // завершено
        ctx.close();
        System.out.println("Travail terminé");
    }

    // список товарів
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // відкриття з'єднання
            connexion = dataSource.getConnection();
            // початок транзакції
            connexion.setAutoCommit(false);
...
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(null, null, connexion);
        }
    }
...
}
  • рядок 14: джерело даних. Зазначимо, що воно має тип [javax.sql.DataSource], який є інтерфейсом;
  • рядок 18: створення екземплярів об’єктів Spring;
  • рядок 20: отримання посилання на джерело даних. Зазначимо, що ніде не згадується клас, який фактично використовується. Отже, тут ніщо не вказує на те, що використовується реалізація [TomcatJdbc];
  • рядок 36: отримання відкритого з’єднання;
  • решта коду ідентична коду класу [IntroJdbc01];

6.6. Conclusion

Більше інформації про управління базами даних можна знайти в документі [Exploiter une base relationnelle avec l'écosystème Spring].