Skip to content

6. [Cours]: Wprowadzenie do API JDBC

Słowa kluczowe: relacyjne bazy danych, API JDBC, SQLException.

6.1. Support

Folder [support / chap-06] zawiera projekty Eclipse z tego rozdziału.

6.2. Architecture

Warstwa JDBC (Java DataBase Connectivity) stanowi uniwersalny interfejs dostępu do baz danych. Zawsze udostępnia ten sam interfejs warstwie [DAO]. W przypadku zmiany warstwy SGBD wystarczy zmienić sterownik JDBC. Warstwa [DAO] pozostaje bez zmian.

6.3. Etapy obsługi bazy danych

W powyższej architekturze obsługa bazy danych przez program konsolowy obejmuje następujące etapy:

  1. załadowanie sterownika bazy danych JDBC;
  2. nawiązanie połączenia z bazą;
  3. wysłanie polecenia SQL do bazy danych oraz przetworzenie wyników polecenia SQL;
  4. zamknięcie połączenia;

Krok 1 wykonuje się tylko raz. Kroki 2–4 wykonuje się wielokrotnie. Należy zauważyć, że nie pozostawia się otwartego połączenia. Zamyka się je, gdy tylko przestaje być potrzebne.

6.3.1. krok 1 – załadowanie sterownika JDBC do pamięci

Kod


        // ładowanie sterownika JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
             // obsługa wyjątku
}

Operacja w wierszu 3 ma na celu załadowanie do pamięci sterownika JDBC z bazy danych. Operację tę należy wykonać tylko raz. Jej powtórzenie nie powoduje jednak błędu. Klasa sterownika JDBC jest wyszukiwana w ścieżce Classpath projektu. W związku z tym w projekcie Eclipse plik [jar] zawierający klasę sterownika JDBC musi zostać dodany do ścieżki Classpath projektu.

6.3.2. Krok 2 – nawiązanie połączenia

Po zainstalowaniu sterownika JDBC należy polecić mu otwarcie połączenia ze sterownikiem BD:

Kod


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 {
            // otwarcie połączenia
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // obsługa wyjątku
            ...
        } finally {
         // zamknięcie połączenia
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // obsługa wyjątku
                ...
            }
         }
}
  • wiersze 3–7: klasy implementujące interfejs JDBC znajdują się w pakiecie [java.sql]. Ponadto w przypadku błędu wszystkie one generują wyjątek typu [SQLException] (wiersze 19, 27). Wyjątek ten pochodzi od klasy [Exception] i jest tzw. wyjątkiem kontrolowanym: konieczne jest zastosowanie bloku try/catch w celu jego obsługi lub, alternatywnie, rezygnacja z obsługi i wskazanie, że metoda pozwala na wystąpienie wyjątku poprzez uzupełnienie sygnatury metody o [throws SQLException];
  • wiersz 17, [DriverManager.getConnection] to metoda statyczna, która oczekuje trzech parametrów:
    • [url]: wartość URL z bazy danych. Jest to ciąg znaków zależny od użytej wartości BD. W przypadku MySQL ma ona postać [jdbc:mysql://localhost:3306/nom_de_la_bd];
    • [user]: właściciel połączenia;
    • [passwd]: jego hasło;
  • wiersze 24–30: połączenie musi zostać zamknięte w klauzuli [finally], aby zostało zamknięte niezależnie od tego, czy wystąpi wyjątek, czy nie.

6.3.3. krok 3 – wysyłanie poleceń SQL i [SELECT]

Po nawiązaniu połączenia można wysyłać polecenia SQL. Sposób obsługi poleceń odczytu [SELECT] różni się od sposobu stosowanego w przypadku operacji aktualizacji [UPDATE, INSERT, DELETE]. Zaczynamy od poleceń SQL i [SELECT]:

Kod


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // rozpoczęcie sesji
            connexion = DriverManager.getConnection(url, user, passwd);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // w trybie tylko do odczytu
            connexion.setReadOnly(true);
            // odczyt tabeli [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)));
            }
            // zatwierdzenie transakcji
            connexion.commit();
        } catch (SQLException e1) {
            // obsługa wyjątku
             doCatchException(connexion,e1);
        } finally {
            // obsługa bloku finally
            doFinally(rs, ps, connexion);
        }

    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • wiersze 8, 10: otwarcie transakcji (wiersz 8) w trybie tylko do odczytu (wiersz 10). Transakcja to sekwencja poleceń SQL, które albo wszystkie zakończą się powodzeniem, albo wszystkie zakończą się niepowodzeniem. Zatem w transakcji zawierającej N poleceń SQL, jeśli polecenie I+1 zakończy się niepowodzeniem, wówczas poprzednie I polecenia zostaną anulowane. W przypadku operacji odczytu transakcja nie jest konieczna. Niemniej jednak utworzenie transakcji tylko do odczytu może umożliwić niektórym poleceniom SGBD przeprowadzenie pewnych optymalizacji;
  • wiersz 12: użycie polecenia [PreparedStatement]. Polecenie [PreparedStatement] zazwyczaj posiada parametry oznaczone znakiem ?. Tutaj ich nie ma. [PreparedStatement] to polecenie przygotowane przez SGBD. Przygotowanie to wiąże się z kosztem i odbywa się tylko raz. Następnie to przygotowane polecenie jest wykonywane przez SGBD z różnymi parametrami rzeczywistymi, które zastąpią parametry formalne „?”. Należy zauważyć, że lepiej jest podawać nazwy żądanych kolumn niż używać symbolu * w celu uzyskania wszystkich kolumn. Określając nazwy kolumn, można następnie uzyskać ich wartości na podstawie ich pozycji w zapytaniu SELECT;
  • wiersz 13: wykonanie zapytania [PreparedStatement]. Pobieramy obiekt typu [ResultSet];

Obiekt typu [ResultSet] reprezentuje tabelę, czyli zbiór wierszy i kolumn. W danym momencie mamy dostęp tylko do jednego wiersza tabeli, zwanego wierszem bieżącym. Podczas początkowego tworzenia obiektu [ResultSet] nie ma wiersza bieżącego. Aby ją uzyskać, należy wykonać operację [ResultSet.next()]. Sygnatura metody next jest następująca:

    boolean next()

Metoda ta próbuje przejść do następnego wiersza w [ResultSet] i zwraca true w przypadku powodzenia, a false w przeciwnym razie. W przypadku powodzenia następny wiersz staje się nowym wierszem bieżącym. Poprzedni wiersz zostaje utracony i nie ma możliwości cofnięcia się, aby go odzyskać.

Tabela o nazwie [ResultSet] zawiera kolumny o nazwach labelCol1, labelCol2, ... określone w wykonanym zapytaniu [SELECT]. Przy użyciu zapytania:

SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
  • kolumna [ID] trafi do kolumny w tabeli [ResultSet] o nazwie [myId];
  • kolumna [NOM] zostanie przeniesiona do kolumny w tabeli [ResultSet] o nazwie [myNom];
  • ...

W powyższym przykładzie identyfikatory [myCol] nazywane są etykietami kolumn. W przypadku braku tych etykiet nazwy kolumn w tabeli [ResultSet] są zależne od tabeli SGBD. Gdy [SELECT] działa na jednej tabeli, etykiety kolumn będą domyślnie nazwami kolumn żądanych przez SELECT. Problem pojawia się, gdy [SELECT] działa na wielu tabelach, a w tych tabelach występują identyczne nazwy kolumn, jak w poniższym przykładzie:

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

zakładając, że tabela [PRODUITS] posiada klucz obcy do tabeli [CATEGORIES], symbolizowany przez relację [Produits].CATEGORIE_ID --> [CATEGORIES].ID, a tabele [PRODUITS] i [CATEGORIES] posiadają obie pole [NOM]. W tym przypadku nazwy nadane w tabeli [ResultSet] kolumnom [PRODUITS.NOM] i [CATEGORIES.NOM] są zależne od tabeli SGBD. Aby zapewnić przenoszalność między plikami SGBD, należy zatem użyć tutaj etykiet kolumn i zapisać:


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

Aby wykorzystać różne pola bieżącego wiersza pliku [ResultSet], dostępne są następujące metody:

Type getType("labelColi") 

aby uzyskać kolumnę o nazwie „labelColi” z bieżącego wiersza, a tym samym kolumnę z pliku [SELECT] posiadającą tę etykietę. Type określa typ pola coli. Można użyć następujących metod [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Zamiast nazwy kolumny można użyć jej pozycji w wykonanym zapytaniu [SELECT]:

Type getType(i) 

gdzie i to indeks żądanej kolumny (i>=1).

  • wiersze 15–17: pobieranie wartości odczytanych w zapytaniu BD;
  • wiersz 19: transakcja zostaje zatwierdzona (mówi się również o jej „commit”); powoduje to jej zakończenie i zwolnienie zasobów, które transakcja SGBD dla niej zablokowała;
  • wiersz 25: zasoby są zwalniane w transakcji [finally]. Ta z kolei wywołuje następującą metodę [doFinally]:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // zamknięcie ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // zamknięcie [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // zamknięcie połączenia
                connexion.close();
            } catch (SQLException e3) {
                 // obsługa wyjątku
            }
        }
    }
  • wiersze 3–9: zamknięcie [ResultSet];
  • wiersze 11–17: zamknięcie [PreparedStatement];
  • wiersze 18–27: zamknięcie połączenia;

Zamknięcia w wierszach 3–17 wydają się zbędne, ponieważ połączenie jest zamykane w wierszach 18–25. W rzeczywistości w niektórych przypadkach nie są one zbędne i zaleca się pozostawienie ich w pliku [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].

  • wiersz 22: wyjątek jest obsługiwany przez następującą metodę [doCatchException]:

    private static void doCatchException(Connection connexion, Throwable th) {
        // anulowanie transakcji
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // obsługa wyjątku
        }
}
  • wiersze 4–6: transakcja zostaje anulowana. To ją kończy, a metoda SGBD będzie mogła zwolnić zasoby przydzielone do jej realizacji;

6.3.4. etap 3 – wysłanie poleceń SQL i [INSERT, UPDATE, DELETE]

Polecenia SQL i [INSERT, UPDATE, DELETE] są operacjami aktualizującymi: modyfikują bazę danych, ale nie zwracają żadnych wierszy. Jedyną zwracaną informacją jest liczba wierszy, których dotyczy operacja aktualizacji.

Kod


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(url, user, passwd);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // w trybie odczytu/zapisu
            connexion.setReadOnly(false);
            // aktualizacja tabeli
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // kategoria 1
            ps.setInt(1, 10);
            // wykonanie
            int nbLignes=ps.executeUpdate();
            // zatwierdzenie transakcji
            connexion.commit();
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException(connexion, e1);
        } finally {
            // przetwarzanie bloku finally
            doFinally(null, ps, connexion);
        }
    }
  • wiersz 9: połączenie jest wykorzystywane do odczytu i zapisu;
  • wiersz 11: operacja [PreparedStatement] z jednym parametrem (oznaczonym symbolem ?). Parametrów może być kilka. Są one numerowane od 1;
  • wiersz 13: przypisuje się wartość do jedynego parametru. Pierwszy parametr [setType] to pozycja parametru w [PreparedStatement] (1, 2, ...), a drugi to przypisana mu wartość. Można użyć metod [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • wiersz 15: stosuje się metodę [executeUpdate], a nie [executeQuery], która jest zarezerwowana dla zleceń SELECT. Metoda zwraca liczbę wierszy, których dotyczy operacja. Może wynosić 0.
  • wiersz 17: transakcja została zatwierdzona;

6.3.5. krok 4 – zamknięcie połączenia

W środowisku wieloużytkownikowym połączenie należy zamknąć jak najszybciej, ponieważ transakcja SGBD dopuszcza ograniczoną liczbę otwartych połączeń. W poprzednich przykładach połączenie było zamykane w klauzuli [finally] operacji SQL, tak aby zostało zamknięte niezależnie od tego, czy wystąpił wyjątek, czy nie.

6.4. Przykładowy projekt

6.4.1. Wsparcie

Folder [support / chap5] zawiera projekty Eclipse z niniejszego rozdziału [1, 2]. Folder [database] zawiera skrypt SQL umożliwiający utworzenie przykładowej bazy danych MySQL z niniejszego rozdziału [1, 3].

6.4.2. Wykorzystywana baza danych

Poniższe przykłady wykorzystują następującą bazę danych MySQL:

 
  • [ID]: klucz główny w trybie AUTO_INCREMENT (jeśli nie podano klucza głównego, system SGBD generuje go);
  • [NOM]: nazwa produktu – unikalna;
  • [CATEGORIE]: numer kategorii;
  • [PRIX]: cena;
  • [DESCRIPTION]: opis produktu;

Utworzymy ją za pomocą narzędzia [WampServer] w następujący sposób: [1-9]:

6.4.3. Projekt Eclipse

  

Projekt jest projektem Maven zdefiniowanym przez następujący plik [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>
  • wiersze 8–12: sterownik JDBC dla SGBD MySQL5;
  • wiersze 13–17: biblioteka obsługująca format jSON (Javascript Object Notation) (patrz punkt 22.6). Wykorzystamy ją do wyświetlania produktów z bazy danych w formacie jSON;

6.4.4. Klasa produktów

Klasa [Produit] ma następujący wygląd:


package istia.st.jdbc;

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

public class Produit {

    // pola
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;

    // konstruktory
    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;
    }

    // metody pobierające i ustawiające
    ...

    // na String
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}
  • wiersz 34: wykorzystujemy bibliotekę jSON do wyświetlenia ciągu znaków jSON produktu. Daje to wyświetlenie podobne do poniższego:
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"}

Zaletą powyższej metody [toString] jest to, że nawet jeśli dodamy lub usuniemy pola z klasy, jej metoda [toString] pozostaje nadal poprawna. Ponadto, jeśli pola same w sobie są obiektami (listy, tablice, słowniki, obiekty użytkownika), biblioteki jSON potrafią z kolei przekształcić je w ciąg znaków jSON;

6.4.5. Klasa [Static]

Klasa [Static] grupuje w metodach kod często używany w klasie głównej:


package istia.st.jdbc;

import java.util.ArrayList;
import java.util.List;

public class Static {

    public static List<String> getErreursFromThrowable(Throwable th) {
        // pobieranie listy komunikatów o błędach wyjątku
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // komunikat o błędzie obiektu typu throwable
            erreurs.add(th.getMessage());
            // przechodzimy do przyczyny obiektu typu throwable
            th = th.getCause();
        }
        // wynik
        return erreurs;
    }
    
    public static void show(String title, List<String> messages){
        // tytuł
        System.out.println(String.format("%s : ",title));
        // komunikaty
        for(String message : messages){
            System.out.println(String.format("- %s",message));
        }
    }
}
  • wiersze 8–19: umożliwia uzyskanie listy błędów zamkniętych w obiekcie typu [Throwable], który jest klasą nadrzędną klasy [Exception];
  • wiersze 21–28: wyświetla na ekranie listę komunikatów;

Kod ten mógłby znajdować się w klasie głównej, ponieważ w tym przypadku jest to jedyna klasa, która go wykorzystuje. Rozpatrujemy tutaj jednak szerszy przypadek, w którym inne klasy mogłyby potrzebować tego kodu.

6.4.6. Szkielet klasy głównej


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 {

    // stałe
    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) {
        // ładowanie sterownika JDBC z MySQL
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // opróżniono tabelę [PRODUITS]
        delete();
        // wypełnianie tabeli
        insert();
        // odczyt tabeli
        select();
        // aktualizacja
        update();
        // wyświetlanie
        select();
        // wstawianie dwóch identycznych elementów
        // wstawianie musi się nie powieść i żaden z tych dwóch elementów nie zostanie wstawiony z powodu transakcji
        insert2();
        // sprawdzamy
        select();
        // koniec
        System.out.println("Travail terminé");
    }

    // lista produktów
    private static void select() {
...
    }

    // usuwanie produktów
    public static void delete() {
...
    }

    // dodawanie produktów
    public static void insert() {
...
    }

    // dodanie 2 produktów
    public static void insert2() {
...
    }

    // aktualizacja niektórych produktów
    public static void update() {
..
    }

    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // zamknięcie ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // zamknięcie [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        // zamknięcie połączenia
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // wyświetlane są komunikaty o błędach
                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) {
        // wyświetla się komunikat o błędzie
        Static.show(title, Static.getErreursFromThrowable(th));
        // anulowanie transakcji
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // wyświetlanie komunikatów o błędach
            Static.show(title, Static.getErreursFromThrowable(e2));
        }
    }
}

6.4.7. Usunięcie zawartości tabeli produktów

Metoda [delete] usuwa zawartość tabeli:


// usunięcie produktów
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(url, user, passwd);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // opróżnianie tabeli [PRODUITS]
            ps = connexion.prepareStatement(delete);
            ps.executeUpdate();
            // zatwierdzenie transakcji
            connexion.commit();
            // powrót do trybu domyślnego
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // przetwarzamy blok finally
            doFinally(null, null, connexion);
        }
    }

W tym przykładzie wykorzystano transakcje. Transakcja pozwala zgrupować polecenia SQL, które muszą zostać wykonane pomyślnie lub anulowane. Należy zapoznać się z czterema operacjami:

  • rozpoczęcie transakcji: [connexion.setAutoCommit(false)];
  • pomyślne zakończenie transakcji: [connexion.commit()]. W tym przypadku wszystkie operacje wykonane na BD podczas transakcji są zatwierdzone;
  • niepowodzenie transakcji: [connexion.rollback()]. W tym przypadku wszystkie operacje wykonane na BD podczas transakcji zostają anulowane;
  • powrót do trybu [auto-commit], który jest trybem domyślnym dla API JDBC: [connexion.setAutoCommit(true)]. W tym trybie każde polecenie SQL stanowi przedmiot transakcji. Jeśli więc wykonamy dwa wstawienia, z których drugie zakończy się niepowodzeniem:
    • w trybie [AutoCommit=true] pierwsze wstawienie pozostaje (zostało zatwierdzone przez pierwsze AutoCommit);
    • w trybie [AutoCommit=false] pierwsze wstawienie zostaje cofnięte;

W naszych przykładach za każdym razem, gdy wystąpi wyjątek, anulujemy transakcję w metodzie [doCatchException]:


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // wyświetlanie komunikatów o błędach
        Static.show(title, Static.getErreursFromThrowable(th));
        // anulowanie transakcji
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // wyświetlanie komunikatów o błędach
            Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
        }
}

6.4.8. Tworzenie zawartości tabeli produktów

Metoda [insert] tworzy zawartość tabeli:


// dodawanie produktów
    public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(url, user, passwd);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // wypełnianie tabeli
            ps = connexion.prepareStatement(insert);
            for (int i = 0; i < 10; i++) {
                // przygotowanie
                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));
                // wykonanie
                ps.executeUpdate();
            }
            // zatwierdzenie transakcji
            connexion.commit();
            // powrót do trybu domyślnego
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // obsługa bloku finally
            doFinally(null, null, connexion);
        }
    }

6.4.9. Wyświetlanie zawartości tabeli produktów

Metoda [select] wyświetla zawartość tabeli:


    // lista produktów
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(url, user, passwd);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // odczyt tabeli [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)));
            }
            // zatwierdzenie transakcji
            connexion.commit();
            // powrót do trybu domyślnego
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // przetwarzanie bloku finally
            doFinally(null, null, connexion);
        }
}

6.4.10. Aktualizacja zawartości tabeli

Metoda [update] aktualizuje niektóre produkty:


// aktualizacja niektórych produktów
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(url, user, passwd);
            // początek transakcji
            connexion.setAutoCommit(false);
            // aktualizacja tabeli
            ps = connexion.prepareStatement(update);
            // kategoria 1
            ps.setInt(1, 1);
            // wykonanie
            ps.executeUpdate();
            // zatwierdzenie transakcji
            connexion.commit();
            // powrót do trybu domyślnego
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // przetwarzanie bloku finally
            doFinally(null, null, connexion);
        }
    }

6.4.11. Rola transakcji

Metoda [insert2] wstawia do tabeli dwa produkty o tym samym kluczu głównym, co jest niemożliwe. Ponieważ operacja odbywa się w ramach transakcji, pierwsze wstawienie zostanie cofnięte.


// dodanie  2 produktów o tych samych kluczach głównych
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(url, user, passwd);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // dodano 1 wiersz
            ps = connexion.prepareStatement(insert2);
            // wykonanie
            ps.executeUpdate();
            // dodano ten sam wiersz po raz drugi, a więc z tym samym kluczem głównym
            // wstawianie musi się nie powieść i żaden z tych dwóch elementów nie może zostać wstawiony z powodu transakcji
            ps.executeUpdate();
            // zatwierdzenie transakcji
            connexion.commit();
            // powrót do trybu domyślnego
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // obsługujemy wyjątek
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout", connexion, e1);
        } finally {
            // przetwarzanie bloku finally
            doFinally(null, null, connexion);
        }
    }

6.4.12. Wyniki

Wyniki wykonania metody [main] są następujące:

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. Korzystanie ze źródła danych typu [DataSource]

Wrócimy do poprzedniej aplikacji, wykorzystując źródło danych typu [javax.sql.DataSource]:

Image

Wykorzystamy źródło danych zaimplementowane przez klasę [org.apache.tomcat.jdbc.pool.DataSource]. Klasa ta korzysta z puli połączeń, czyli zbioru otwartych połączeń:

  • gdy pula jest instancjonowana, otwierana jest określona liczba połączeń z bazą danych. Liczbę tę można skonfigurować;
  • gdy kod Java otwiera połączenie, jest ono pobierane z puli;
  • gdy kod Java zamyka połączenie, jest ono zwracane do puli;

W rezultacie połączenia są otwierane tylko raz, co poprawia wydajność dostępu do bazy danych. Źródło danych zostanie zdefiniowane w klasie konfiguracyjnej Spring

6.5.1. Projekt Eclipse

  

Projekt jest projektem Maven zdefiniowanym przez następujący plik [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>
        <!-- biblioteka 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>
  • wiersze 21–25: zależność od Spring;
  • wiersze 27–31: zależność od biblioteki dostarczającej źródło danych;

Klasa konfiguracyjna Springa [AppConfig] wygląda następująco:


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 {

    // stałe
    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() {
        // źródło danych TomcatJdbc
        DataSource dataSource = new DataSource();
        // konfiguracja dostępu JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // początkowo otwarte połączenie
        dataSource.setInitialSize(1);
        // wynik
        return dataSource;
    }
}
  • wiersze 11–19: stałe zdefiniowane wcześniej w [IntroJdbc01] zostały przeniesione do [AppConfig];
  • wiersze 31–34: bean Spring definiujący źródło danych;
  • wiersz 24: utworzenie źródła danych, które nie zostało jeszcze skonfigurowane;
  • wiersze 26–29: informacje umożliwiające źródłu danych połączenie się z bazą danych;
  • wiersz 31: tworzy pulę zawierającą 1 połączenie. W tym przypadku nie potrzebujemy więcej. Nigdy nie występuje wiele połączeń jednocześnie;

6.5.2. Klasa główna

Główna klasa [IntroJdbc02] wygląda następująco:


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 {
    // źródło danych
    private static DataSource dataSource;

    public static void main(String[] args) {
        // pobieranie kontekstu Spring
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // pobieranie źródła danych
        dataSource = ctx.getBean(DataSource.class);
        // opróżnianie tabeli [PRODUITS]
        delete();
...
        // koniec
        ctx.close();
        System.out.println("Travail terminé");
    }

    // lista produktów
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // nawiązanie połączenia
            connexion = dataSource.getConnection();
            // początek transakcji
            connexion.setAutoCommit(false);
...
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // przetwarzanie bloku finally
            doFinally(null, null, connexion);
        }
    }
...
}
  • wiersz 14: źródło danych. Należy zauważyć, że jest to typ [javax.sql.DataSource], który jest interfejsem;
  • wiersz 18: instancjonowanie obiektów Spring;
  • wiersz 20: uzyskanie odwołania do źródła danych. Należy zauważyć, że w żadnym momencie nie podano nazwy faktycznie używanej klasy. W tym przypadku nic nie wskazuje na to, że używana jest implementacja [TomcatJdbc];
  • wiersz 36: uzyskanie otwartego połączenia;
  • pozostała część kodu jest identyczna z kodem klasy [IntroJdbc01];

6.6. Conclusion

Więcej informacji na temat zarządzania bazami danych można znaleźć w dokumencie [Exploiter une base relationnelle avec l'écosystème Spring].