Skip to content

3. Introducción a API JDBC

3.1. Configuración del entorno de trabajo

Vamos a trabajar con una base de datos MySQL5.

Debe tener:

  • tener instalado un JDK (Java Development Kit) (apartado 23.1);
  • tener instalado el gestor de dependencias Maven (apartado 23.2);
  • tener instalado el IDE Spring Tool Suite (STS) (apartado 23.3);
  • instalado el SGBD MySQL5 (apartado 23.4) y su cliente EMS MyManager (apartado 23.5);
  • descargado los códigos del documento [http://tahe.developpez.com/java/spring-database];

A partir de ahora se supone que el administrador de MySQL5 es root con la contraseña root. Inicie SGBD, MySQL5 y su cliente [MyManager]. Con ayuda de [MyManager], creamos la base de datos [dbproduits] [1-34]:

  • en [3], la base debe llamarse [dbproduits];
  • en [8-9], root con la contraseña root (lo que no se muestra en la captura de pantalla anterior);
  • en [14a], la contraseña es root de nuevo (lo que no se muestra en la captura de pantalla);
  • en [15], se ha creado la base de datos [dbproduits];
  • en [20], preste atención a la base seleccionada. Debe ser la base [dbproduits];
  • en [22], la carpeta es <ejemplos>/spring-database-config/mysql/databases, donde <ejemplos> es la carpeta de los ejemplos descargados;
  • en [23], seleccione el script SQL [dbproduits.sql]. Generará la tabla [PRODUITS] en la base de datos [dbproduits];
  • en [30], se ha creado la tabla [produits];
  • en [33], las columnas de la tabla [produits];
  • en [34], inicialmente está vacía;

Ahora, con STS, importa los siguientes proyectos (sigue el procedimiento utilizado para los proyectos de la carpeta <ejemplos>/spring-core):

  • en [2], el proyecto [mysql-config-jdbc] se encontrará en la carpeta [<exemples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1];

Este proyecto configura la capa JDBC de la arquitectura siguiente:

A continuación, vuelva a importar los tres proyectos siguientes:

  • en [2]; los proyectos se encontrarán en la carpeta [<exemples>/spring-database-config/spring-jdbc] [1];

Estos tres proyectos son proyectos Maven que utilizan el proyecto Maven [mysql-config-jdbc]. Este último proyecto genera el siguiente artefacto Maven (véase pom.xml):


    <groupId>dvp.spring.database</groupId>
    <artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>

El proyecto [oracle-config-jdbc, db2-config-jdbc, ...] generará el mismo artefacto. Para asegurarse de que los proyectos [spring-generic-jdbc-*] cargados actualmente en STS utilizan efectivamente el proyecto [mysql-config-jdbc]:

  • asegúrese de que no haya otro proyecto [sgbd-config-jdbc] cargado al mismo tiempo. Esto podría provocar errores difíciles de comprender;
  • actualice la configuración de Maven de los proyectos cargados de la siguiente manera:

Para verificar su configuración, ejecute la configuración de ejecución [spring-jdbc-generic-01.IntroJdbc01] [1-3]:

Deberías obtener los siguientes resultados en la consola:


------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.00000000000001,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.10000000000001,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.2,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.30000000000001,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire : 
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Travail terminé

En los siguientes ejemplos, el lector podrá:

  • trabajar directamente con los proyectos cargados anteriormente;
  • o bien crear los proyectos por sí mismo;

3.2. Pasos para utilizar una base de datos

En la arquitectura anterior, la explotación de una base de datos por parte del programa de consola comprende las siguientes etapas:

  1. carga del controlador JDBC de la base de datos;
  2. apertura de una conexión con la base;
  3. emisión de una orden SQL en la base de datos y procesamiento de los resultados de la orden SQL;
  4. cierre de la conexión;

El paso 1 solo se realiza una vez. Los pasos 2-4 se repiten. Cabe destacar que no se deja una conexión abierta. Se cierra tan pronto como ya no se necesita.

3.2.1. paso 1: carga en memoria del controlador JDBC

El código


        // carga del controlador JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
            // gestionar la excepción
}

La operación de la línea 3 tiene como objetivo cargar en memoria el controlador JDBC de la base de datos. Esta operación solo es necesario realizarla una vez. Sin embargo, repetirla no provoca ningún error. La clase del controlador JDBC se busca en la ruta de clases (Classpath) del proyecto. Por lo tanto, en el proyecto Eclipse, el archivo [jar], que contiene la clase del controlador JDBC, debe haberse incluido en la ruta de clases del proyecto.

3.2.2. Paso 2: apertura de una conexión

Una vez instalado el controlador JDBC, se le pide que abra una conexión con el BD:

El código


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 {
            // apertura de conexión
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // se procesa la excepción
            ...
        } finally {
         // cerrar la conexión
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // gestionar la excepción
                ...
            }
         }
}
  • líneas 3-7: las clases de implementación de la interfaz JDBC se encuentran todas en el paquete [java.sql]. Además, en caso de error, todas lanzan una excepción de tipo [SQLException] (líneas 19 y 27). Esta excepción deriva de la clase [Exception] y es una excepción denominada «controlada»: es obligatorio utilizar un try/catch para gestionarla o, alternativamente, no gestionarla e indicar que el método permite que se produzca la excepción completando la firma del método con [throws SQLException];
  • línea 17, [DriverManager.getConnection] es un método estático que espera tres parámetros:
    • [url]: el URL de la base de datos. Es una cadena de caracteres que depende del BD utilizado. Para MySQL, tiene el formato [jdbc:mysql://localhost:3306/nom_de_la_bd];
    • [user]: el propietario de la conexión;
    • [passwd]: su contraseña;
  • líneas 24-30: la conexión debe cerrarse en la cláusula [finally] para que se cierre independientemente de si hay una excepción o no.

3.2.3. paso 3: emisión de órdenes SQL [SELECT]

Una vez obtenida la conexión, se pueden emitir órdenes SQL. La forma de gestionar las órdenes de lectura [SELECT] difiere de la utilizada para las operaciones de actualización [UPDATE, INSERT, DELETE]. Comenzamos con las órdenes SQL [SELECT]:

El código


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // abrir conexión
            connexion = DriverManager.getConnection(url, user, passwd);
            // inicio de transacción
            connexion.setAutoCommit(false);
            // en modo de solo lectura
            connexion.setReadOnly(true);
            // se lee la tabla [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)));
            }
            // confirmar transacción
            connexion.commit();
        } catch (SQLException e1) {
            // se gestiona la excepción
             doCatchException(connexion,e1);
        } finally {
            // se procesa el finally
            doFinally(rs, ps, connexion);
        }

    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • líneas 8, 10: apertura de una transacción (línea 8) en modo de solo lectura (línea 10). Una transacción es una secuencia de órdenes SQL que o bien se ejecutan todas con éxito o bien fallan todas. Así, en una transacción que contenga N órdenes SQL, si la orden I+1 falla, las I anteriores se anularán. Para una operación de lectura, no es necesaria una transacción. No obstante, crear una transacción de solo lectura puede permitir que algunos SGBD realicen ciertas optimizaciones;
  • línea 12: uso de un [PreparedStatement]. Un [PreparedStatement] suele tener parámetros indicados por el carácter ?. Aquí no los tiene. Un [PreparedStatement] es una orden preparada por el SGBD. Esta preparación tiene un coste y solo se realiza una vez. A continuación, esta orden preparada es ejecutada por el SGBD con diferentes parámetros efectivos que sustituirán a los parámetros formales ?. Cabe señalar que es preferible nombrar las columnas deseadas en lugar de utilizar la notación * para obtener todas las columnas. Al especificar el nombre de las columnas, se pueden obtener sus valores a partir de su posición en la consulta SELECT;
  • línea 13: ejecución de [PreparedStatement]. Se recupera un objeto de tipo [ResultSet];

Un objeto de tipo [ResultSet] representa una tabla, es decir, un conjunto de filas y columnas. En un momento dado, solo se tiene acceso a una fila de la tabla denominada fila actual. Durante la creación inicial de [ResultSet], no hay ninguna fila actual. Es necesario realizar una operación [ResultSet.next()] para obtenerla. La firma del método next es la siguiente:

    boolean next()

Este método intenta pasar a la siguiente línea del [ResultSet] y devuelve true si tiene éxito, false en caso contrario. Si tiene éxito, la siguiente línea se convierte en la nueva línea actual. La línea anterior se pierde y no se podrá volver atrás para recuperarla.

La tabla de [ResultSet] tiene columnas denominadas labelCol1, labelCol2, ... especificadas en la consulta [SELECT] ejecutada. Con la consulta:

SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
  • la columna [ID] irá a una columna de [ResultSet] llamada [myId];
  • la columna [NOM] pasará a una columna de [ResultSet] denominada [myNom];
  • ...

En el ejemplo anterior, los identificadores [myCol] se denominan etiquetas de columna. En ausencia de estas etiquetas, los nombres de las columnas del [ResultSet] dependen del SGBD. Cuando el [SELECT] opera sobre una única tabla, las etiquetas de las columnas serán, por defecto, los nombres de las columnas solicitadas por el SELECT. El problema surge cuando el [SELECT] opera sobre varias tablas y en estas se encuentran nombres de columnas idénticos, como en el siguiente ejemplo:

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

suponiendo que la tabla [PRODUITS] tenga una clave externa hacia la tabla [CATEGORIES] simbolizada por la relación [Produits].CATEGORIE_ID --> [CATEGORIES].ID, y que las tablas [PRODUITS] y [CATEGORIES] tengan ambas un campo [NOM]. En este caso, los nombres asignados en [ResultSet] a las columnas [PRODUITS.NOM] y [CATEGORIES.NOM] dependen de SGBD. Para la portabilidad entre SGBD, hay que utilizar aquí etiquetas de columna, por lo que se escribirá:


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

Para explotar los diferentes campos de la línea actual de [ResultSet], disponemos de los siguientes métodos:

Type getType("labelColi") 

para obtener la columna denominada «labelColi» de la línea actual y, por lo tanto, la columna de [SELECT] que tiene esa etiqueta. «Tipo» se refiere al tipo del campo «colli». Se pueden utilizar los siguientes métodos [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... En lugar de utilizar el nombre de la columna, se puede utilizar su posición en la consulta [SELECT] ejecutada:

Type getType(i) 

donde i es el índice de la columna deseada (i>=1).

  • líneas 15-17: recuperación de los valores leídos en la consulta BD;
  • línea 19: la transacción se valida (también se dice que se confirma). Esto la finaliza y libera los recursos que la SGBD había movilizado para ella;
  • línea 25: se liberan los recursos en [finally]. Este llama al siguiente método [doFinally]:

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

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

            }
        }
        if (connexion != null) {
            try {
                // cerrar la conexión
                connexion.close();
            } catch (SQLException e3) {
                // gestionar la excepción
            }
        }
    }
  • líneas 3-9: cierre del [ResultSet];
  • líneas 11-17: cierre de [PreparedStatement];
  • líneas 18-27: cierre de la conexión;

Los cierres de las líneas 3-17 parecen redundantes, ya que se cierra la conexión en las líneas 18-25. De hecho, en algunos casos no lo son y se recomienda dejarlos [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].

  • línea 22: la excepción se gestiona mediante el siguiente método [doCatchException]:

    private static void doCatchException(Connection connexion, Throwable th) {
        // anulación de la transacción
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // tratar la excepción
        }
}
  • líneas 4-6: la transacción se cancela. Esto la finaliza y SGBD podrá liberar los recursos movilizados para ella;

3.2.4. paso 3: emisión de órdenes SQL [INSERT, UPDATE, DELETE]

Las órdenes SQL y [INSERT, UPDATE, DELETE] son operaciones de actualización: modifican la base de datos, pero no devuelven ninguna línea. La única información que se devuelve es el número de líneas afectadas por la operación de actualización.

El código


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);
        }
    }
  • línea 9: la conexión se utiliza para lectura y escritura;
  • línea 11: un [PreparedStatement] con 1 parámetro (simbolizado por ?). Puede haber varios parámetros. Se numeran a partir del 1;
  • línea 13: se asigna su valor al único parámetro. El primer parámetro de [setType] es la posición del parámetro en el [PreparedStatement] (1, 2, ...) y el segundo, el valor que se le asigna. Se pueden utilizar los métodos [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • línea 15: se utiliza el método [executeUpdate] y no [executeQuery], reservado para las órdenes SELECT. El método devuelve el número de líneas afectadas por la operación. Puede ser 0.
  • línea 17: se valida la transacción;

3.2.5. paso 4: cierre de la conexión

Una conexión debe cerrarse lo antes posible en un contexto multiusuario, ya que un SGBD admite un número limitado de conexiones abiertas. En los ejemplos anteriores, se cerraba en la cláusula [finally] de las operaciones SQL para que se cerrara independientemente de si se producía una excepción o no.

3.3. Configuración de la capa JDBC de SGBD MySQL5

Vamos a estudiar el proyecto [mysql-config-jdbc] que configura la capa JDBC a continuación:

3.3.1. El proyecto Eclipse

 

3.3.2. Configuración de Maven

El archivo [pom.xml] del proyecto es el siguiente:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>generic-config-jdbc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>configuration generic jdbc</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- dependencias variables ********************************************** -->
        <!-- controlador JDBC del SGBD -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- dependencias constantes ********************************************** -->
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- biblioteca jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- Prueba de Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- registros -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>

En esta configuración de Maven se han agrupado una serie de archivos que son necesarios tanto para el proyecto [mysql-config-jdbc] como para los proyectos que se basarán en él:

  • líneas 4-6: el artefacto Maven generado por el proyecto. Como ya se ha dicho, todos los proyectos del tipo [*-config-jdbc] generan este mismo artefacto. Por lo tanto, no deben cargarse al mismo tiempo dos proyectos del tipo [*-config-jdbc];
  • líneas 9-13: el proyecto Maven padre de este. Define las versiones de un gran número de archivos utilizados por el ecosistema Spring. Esto evita tener que especificarlas en los proyectos derivados;
  • líneas 18-21: el archivo del controlador JDBC de SGBD MySQL5. Es el único archivo necesario para el proyecto [spring-jdbc-01];
  • líneas 24-27: el artefacto [tomcat-jdbc] aporta un archivo necesario para los proyectos JDBC y [spring-jdbc-02 à 04];
  • líneas 29-36: aportan las bibliotecas necesarias para la gestión de jSON. Se utilizan en casi todos los proyectos del documento;
  • líneas 38-42: Google Guava es una biblioteca de gestión de colecciones. Se utiliza en casi todos los proyectos del documento;
  • líneas 43-52: las bibliotecas que permiten escribir pruebas que integran Spring y JUnit. Se utilizan en casi todos los proyectos del documento;
  • líneas 54-57: las bibliotecas de registros. Se utilizan en casi todos los proyectos del documento;
  • líneas 67-71: el complemento que permite instalar el artefacto del proyecto [mysql-config-jdbc] en el repositorio local de Maven;

3.3.3. La clase de configuración [ConfigJdbc]

  

La clase [ConfigJdbc] es la siguiente:


package generic.jdbc.config;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

public class ConfigJdbc {

    // paramètres de connexion
    public final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
    public final static String USER_DBPRODUITS = "root";
    public final static String PASSWD_DBPRODUITS = "root";
...
    // ordres SQL [jdbc-01, jdbc-02]
    public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
    //public final static String V1_DELETE_PRODUITS = String.format("DELETE FROM %s", TAB_PRODUITS);
    public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

    // ordres SQL [jdbc-03]
    public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
    public final static String V2_DELETE_ALLPRODUITS = "DELETE FROM PRODUITS";
    public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";
    public final static String V2_SELECT_ALLPRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";
    public final static String V2_SELECT_PRODUIT_BYNAME = "SELECT ID, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE NOM=?";
    public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";

...

}

La clase [ConfigJdbc] sirve para configurar la capa JDBC de los cuatro proyectos [spring-jdbc-01 à 04]. La mayor parte de la configuración se refiere al proyecto [spring-jdbc-04]. Presentaremos esta parte cuando estudiemos dicho proyecto. Arriba solo se ha incluido la configuración de los proyectos [spring-jdbc-01 à 03].

  • líneas 14-17: los parámetros de conexión a la base de datos MySQL5 [dbproduits];
  • líneas 20-25: las órdenes SQL utilizadas en los proyectos [spring-jdbc-01 et 02];
  • líneas 28-34: las órdenes SQL utilizadas en el proyecto [spring-jdbc-03];

Estas órdenes SQL utilizan la tabla [PRODUITS] de la base de datos MySQL5 [dbproduits], cuya estructura es la siguiente:

 
  • [ID]: clave primaria en modo AUTO_INCREMENT (si no se proporciona una clave primaria, SGBD la genera);
  • [NOM]: nombre de un producto - único;
  • [CATEGORIE]: n.º de su categoría;
  • [PRIX]: su precio;
  • [DESCRIPTION]: una descripción del producto;

3.3.4. La clase [Produit]

  

La clase [Produit] es la imagen de una línea de la tabla [PRODUITS]:


package generic.jdbc.entities.dbproduits;

public class Produit {

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

    // constructores
    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 y setters
...
}

Más adelante necesitaremos comparar dos productos para saber si son iguales o no. Diremos que dos productos son iguales si todos sus campos son iguales. Para ello, vamos a redefinir el método [equals] de la clase [Object], de la que deriva la clase [Produit]:


    // método de igualdad
    @Override
    public boolean equals(Object o) {
        // casos sencillos
        if (o == null || o.getClass() != this.getClass()) {
            return false;
        }
        Produit p = (Produit) o;
        return this == o
                || (this.id == p.id && this.nom.equals(p.getNom()) && this.categorie == p.categorie
                        && Math.abs(this.prix - p.prix) < 1e-6 && this.description.equals(p.description));
}
  • línea 3: el método [equals] recibe un objeto o que debe comparar con el objeto this;
  • líneas 5-7: los casos sencillos en los que se puede determinar de inmediato que los dos objetos no son iguales. [Object].getClass() proporciona una instancia del tipo [Class], un tipo que representa la clase real del objeto;
  • línea 8: el objeto o se convierte en el producto p;
  • línea 9: si las dos referencias o y p a un producto son iguales, entonces se trata físicamente del mismo producto;
  • línea 9: si o y p son dos referencias diferentes a dos productos que tienen los mismos campos, diremos que son iguales. Dado que el precio es de tipo [double] y que no existe una representación exacta de los números reales en informática, consideraremos que dos precios son idénticos si difieren en menos de 10-6;

Por otra parte, redefiniremos el método [hasCode] de la clase [Object]:


    // hashcode
    @Override
    public int hashCode() {
        return id + 2 * nom.hashCode() + 3 * categorie + 4 * description.hashCode();
}

Los valores de hashCode de dos productos deben ser iguales si el método [equals] ha declarado que estos dos productos son iguales. Este valor de hashCode se utiliza para distribuir objetos en conjuntos tales como diccionarios. En el ejemplo anterior, si dos productos son idénticos, tendrán el mismo hashCode.

3.3.5. La excepción [UncheckedException]

  

Consideremos la siguiente arquitectura:

  • la capa [JDBC] lanza excepciones de tipo [SQLException]. Esta excepción debe remontar las capas hasta llegar a la capa más alta, en este caso la capa de pruebas;

La capa [DAO] podría limitarse a dejar que la [SQLException] suba hasta la capa de pruebas. Pero como esta excepción no está controlada (deriva directamente de [Exception]), esto implicaría que la interfaz [IDao] de la capa [DAO] fuera la siguiente:


public interface IDao {

    // ajouter des produits
    public List<Produit> addProduits(List<Produit> produits) throws SQLException;

    // liste de tous les produits
    public List<Produit> getAllProduits() throws SQLException;

    // un produit particulier
    public Produit getProduitById(int id) throws SQLException;

    public Produit getProduitByName(String name) throws SQLException;

    // mise à jour de plusieurs produits
    public int updateProduits(List<Produit> produits) throws SQLException;

    // suppression de tous les produits
    public int deleteAllProduits() throws SQLException;

    // suppression de plusieurs produits
    public int deleteProduits(int[] ids) throws SQLException;
}

Y esto es muy molesto, ya que nos impide implementar la interfaz [IDao] mediante una clase que lanzaría una excepción diferente. Para sortear esta dificultad, la capa [DAO] lanzará una excepción [DaoException] no controlada (derivada de [RuntimeException]), lo que nos evita la cláusula [throws] en la firma de los métodos de la interfaz. De este modo, esta podrá ser implementada por cualquier clase que también lance una excepción no controlada, que podrá ser diferente de la excepción [DaoException]. Nuestra arquitectura queda ahora así:

Para facilitar la creación de excepciones no controladas para diferentes capas de una aplicación, creamos una clase padre [UncheckedException]:

  

package generic.jdbc.infrastructure;

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

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

// clase de excepción genérica
// la excepción es no controlada

public class UncheckedException extends RuntimeException {

    // serial ID generado
    private static final long serialVersionUID = -2924871763340170310L;

    // propiedades
    private int code;
    private String trace;
    private List<ShortException> exceptions;

    // constructores
    public UncheckedException() {
        super();
    }

    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // local
        this.code = code;
        this.exceptions = getErreursForException(e);
        // rastreo
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
    }

    @Override
    public String getMessage() {
        return this.toString();
    }

    @Override
    public void printStackTrace() {
        System.out.println(this);
    }

    // lista de mensajes de error de una excepción
    private List<ShortException> getErreursForException(Throwable th) {
        // se recuperan los elementos de la pila de la excepción
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // se recupera la excepción actual
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // siguiente excepción
            cause = cause.getCause();
        }
        return exceptions;
    }

    @Override
    public String toString() {
        ObjectMapper jsonMapper = new ObjectMapper();
        try {
            return String.format("[code=%s, trace=%s, exceptions=%s", code, trace, jsonMapper.writeValueAsString(exceptions));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }

    // getter y setter
...
}
  • línea 12: la clase deriva de [RuntimeException] y, por lo tanto, es un tipo de excepción no controlada. Servirá para encapsular una excepción controlada (SQLException) en un tipo de excepción no controlada (UncheckedException);
  • para diferenciar entre las excepciones de tipo [UncheckedException], se les podrá asignar un código que se almacenará en el campo privado de la línea 18. Un código Java que intercepte una excepción de tipo [UncheckedException] tendrá acceso a este código de error gracias al método [getCode] (líneas 80 y siguientes);
  • línea 20: almacena los mensajes de error de la pila de la excepción encapsulada;
  • líneas 23-43: las diferentes formas de construir un objeto de tipo [UncheckedException];
  • líneas 56-67: un método privado que permite construir la lista de errores de la línea 20 a partir de un objeto [Throwable] o derivado, en particular el tipo [Exception];
  • líneas 69-78: el método [toString] devuelve una cadena de caracteres que representa la excepción. Para mostrar la lista de errores de la línea 20, utiliza una biblioteca jSON. Esta se encuentra en las dependencias Maven del proyecto:

        <!-- biblioteca jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
</dependency>
  • líneas 45-48: redefinen el método [getMessage] de la clase padre [RuntimeException]. Este devuelve aquí la firma [toString] de la clase;
  • líneas 50-53: redefinen el método [printStackTrace] de la clase padre [RuntimeException]. Se mostrará la firma [toString] de la clase;

La clase [UncheckedException] registra en el campo de la línea 20 una lista de excepciones descritas por el siguiente tipo [ShortException]:


package pam.dao.exceptions;

public class ShortException {

    // propiedades
    private String className;
    private String errorMessage;

    // constructores
    public ShortException() {

    }

    public ShortException(String className, String errorMessage) {
        this.className = className;
        this.errorMessage = errorMessage;
    }

    // getters y setters
...
}
  • línea 6: el nombre de la clase de la excepción que se ha producido;
  • línea 7: el mensaje de error asociado;

Examinemos el siguiente constructor de la clase [UncheckedException]:


    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // local
        this.code = code;
        this.exceptions = getErreursForException(e);
        // trace
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
}
  • línea 1, los parámetros son los siguientes:
    • [code]: un código de error;
    • [e]: la excepción que se encapsula. [Throwable] es la clase padre de la clase [Exception] y deriva directamente de la clase [Object]. Es la clase padre de todas las clases C con las que se puede escribir [throw c;], donde c es una instancia de C;
    • [simpleClassName]: el nombre simple de la clase del código de usuario donde se ha detectado la excepción e;
  • línea 4: se registra el código de error;
  • línea 5: la lista de [ShortException] se construye a partir del [Throwable e] pasado como parámetro;
  • líneas 7-16: se examinan lo que se conoce como «trazas de la excepción». Una excepción inicial se produce en un punto concreto del código y luego se remonta hasta el método que llamó al que la generó, y así sucesivamente hasta que un try / catch la detiene. En este recorrido hacia arriba, la excepción inicial deja rastros almacenados en la matriz [e.stackTrace] de la excepción e. Estos se obtienen aquí en la línea 8, a partir del [Throwable e] pasado como parámetro. Cada elemento de tipo [StackTraceElement] es un objeto que tiene, entre sus campos, los siguientes:
    • [fileName]: el nombre del archivo Java donde se produjo la excepción;
    • [lineNumber]: el número de línea de ese archivo en la que se produjo la excepción;
    • [methodName]: el nombre del método en este archivo donde se produjo la excepción;
  • las líneas 10-16 buscan en la tabla de trazas de la excepción pasada como parámetro la primera aparición de la condición [trace.fileName==simpleClassName.java], donde [simpleClassName] es el tercer parámetro del constructor. La idea es recordar dónde se produjo la excepción en el código de usuario. Este encapsulará una excepción de la siguiente manera:
1
2
3
4
5
6
7
try{
// código que puede lanzar una excepción controlada
...
}catch(UnTypeDexception e){
// encapsulamos la excepción controlada e en una excepción no controlada
    throw new UncheckedException(189,e,getClass().getSimpleClassName())
}
  • línea 13: se crea una cadena de caracteres de tipo [fileName, methodName, lineNumber] que caracteriza el lugar del código de usuario donde se detuvo la excepción;

Ahora, examinemos el código que registra la lista de excepciones de la pila de excepciones de la excepción [Throwable th] encapsulada por el constructor anterior:


    // lista de mensajes de error de una excepción
    private List<ShortException> getErreursForException(Throwable th) {
        // se recuperan los elementos de la pila de la excepción
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // se recupera la excepción actual
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // excepción siguiente
            cause = cause.getCause();
        }
        return exceptions;
}

Durante su propagación hacia el método que la detuvo mediante un try/catch, la excepción inicial e pudo encapsularse en una excepción. Es entonces esta última la que realiza su propagación hacia el método que la detendrá definitivamente. Por lo tanto, ella también puede ser objeto de encapsulación. Al final, cuando un método decide detener una excepción th y explotarla, encontrará la excepción inicial e enterrada en el fondo de una pila de excepciones. Así, en el ejemplo anterior, el parámetro [Throwable th] no es más que la punta del iceberg de las excepciones. Su atributo [th.cause] permite conocer la excepción que ella misma encapsula. Y así sucesivamente. Cuando una excepción e es tal que [e.getCause()==null], significa que e es la excepción inicial.

  • línea 8: para cada excepción de la pila de excepciones de [Throwable th], se almacenan dos datos:
    • [getClass().getName()]: el nombre completo de la excepción;
    • [getMessage()]: el mensaje de error asociado;

3.4. Ejemplo-01

3.4.1. La arquitectura del proyecto

En este ejemplo, un programa de consola utiliza la interfaz de la capa [JDBC].

3.4.2. El proyecto Eclipse

Creamos un proyecto Spring/Maven [spring-jdbc-01] siguiendo los pasos del apartado 2.5.2.1.

  

El proyecto es un proyecto Maven definido por el siguiente archivo [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-01</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- búsqueda del padre en el repositorio -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- configuración JDBC del SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • líneas 28-32: el proyecto utiliza el artefacto [generic-config-jdbc] del proyecto [mysql-config-jdbc] que acabamos de estudiar. Por lo tanto, el proyecto [spring-jdbc-01] tiene acceso a todos los elementos del proyecto [mysql-config-jdbc];

Podemos ver este último punto de dos maneras al inspeccionar las dependencias Maven del proyecto:

  • en [2], vemos que el proyecto [mysql-config-jdbc] se encuentra entre las dependencias Maven del proyecto. Dado que estas últimas se encuentran en la ruta de clases del proyecto, esto significa que el proyecto [mysql-config-jdbc] también está en dicha ruta de clases y que, por lo tanto, sus clases e interfaces son visibles en el proyecto [spring-jdbc-01];

El proyecto Maven [mysql-config-jdbc] no necesita estar presente en la pestaña [Package Explorer] para que otros proyectos Maven puedan utilizarlo. Basta con que esté presente en el repositorio local de Maven. A diferencia de un IDE como Netbeans, esta presencia no es automática con Eclipse. Hay que forzarla:

Hemos visto las condiciones que hacen posible esta generación en el apartado 2.3.5. Una vez realizada, se puede eliminar el proyecto [mysql-config-jdbc] de la pestaña [Package Explorer]:

  • no se debe marcar [3], ya que elimina físicamente el proyecto del disco, lo que lo hace irrecuperable;

Esta operación reinicia el cálculo de las dependencias Maven de los proyectos que dependen del proyecto extraído de [Package Explorer]. Esto cambia la rama [Maven Dependencies] de dichos proyectos. Por ejemplo, para el proyecto [spring-jdbc-01], la rama [Maven Dependencies] pasa a ser la siguiente:

En esta ocasión, la dependencia ya no recae sobre un proyecto, sino sobre el artefacto Maven del mismo, en este caso el artefacto [generic-config-jdbc] [1]. Se puede ver que tenemos acceso a todas las clases e interfaces de este artefacto. Como se ha dicho, este artefacto será generado por todos los proyectos [*-config-jdbc]. Para evitar errores, nosotros:

  • mantendremos siempre un único proyecto [*-config-jdbc] en la pestaña [Package Explorer];
  • actualizaremos la configuración de Maven de todos los proyectos de la pestaña [Package Explorer] (Alt-F5) para que estos incluyan en sus dependencias de Maven el proyecto [*-config-jdbc] utilizado;

3.4.3. El esqueleto de la clase principal

  

El esqueleto de la clase principal [IntroJdbc01] es el siguiente:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

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

public class IntroJdbc01 {

    // constantes
    final static ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) {
        // carga del controlador JDBC del SGBD
        try {
            Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // se vacía la tabla [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // se rellena
        System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
        insert();
        // se lee
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // actualización
        System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
        update();
        // visualización
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // se vacía la tabla [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // se muestra
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // INSERTion de dos elementos idénticos
        // el INSERTion debe fallar y ninguno de los dos elementos se inserta debido a la transacción
        System.out.println(String.format("------------------------------ %s",
                "Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
        insert2();
        // se comprueba
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // fin
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // lista de productos
    private static void select() {
    ...
    }

    // visualización jSON de un objeto
    private static void affiche(Object object) {
...
    }

    // eliminación de productos
    public static void delete() {
...
    }

    // Añadir productos
    public static void insert() {
...
    }

    // adición de 2 productos con las mismas claves primarias
    public static void insert2() {
...
    }

    // actualización de algunos productos
    public static void update() {
...
    }

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

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

            }
        }
        if (connexion != null) {
            try {
                // cerrar la conexión
                connexion.close();
            } catch (SQLException e3) {
                // se muestran los mensajes de error
                show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
                        getErreursFromThrowable(e3));
            }
        }
    }

    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // se muestran los mensajes de error
        show(title, getErreursFromThrowable(th));
        // cancelación de la transacción
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // se muestran los mensajes de error
            show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
        }
    }

    private static List<String> getErreursFromThrowable(Throwable th) {
        // se recupera la lista de mensajes de error de la excepción
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // mensaje de error del throwable
            erreurs.add(th.getMessage());
            // se pasa a la causa del throwable
            th = th.getCause();
        }
        // resultado
        return erreurs;
    }

    private static void show(String title, List<String> messages) {
        // título
        System.out.println(String.format("%s : ", title));
        // mensajes
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
}
  • líneas 23-29: carga del controlador JDBC desde SGBD. En la línea 25, se utiliza la constante [ConfigJdbc.DRIVER_CLASSNAME] definida en el proyecto [mysql-config-jdbc];
  • líneas 136-147: el método [getErreursFromThrowable] devuelve la lista de mensajes de error encapsulados en un objeto de tipo [Throwable], que es la clase padre de la clase [Exception]. Una excepción puede contener otra, a la que se accede mediante el método [Throwable].getCause(). De este modo, se revisan todas las excepciones encapsuladas en el objeto [Throwable];
  • líneas 149-156: el método [show(String title, List<String> messages)] muestra los mensajes precedidos por el texto [title];
  • líneas 122-134: el método [doCatchException(String title, Connection connexion, Throwable th))] gestiona las excepciones encontradas por los métodos de la clase. La excepción gestionada está representada por el parámetro [Throwable th]. El objetivo del método es:
    • anular la transacción en curso del objeto [Connection connexion] (líneas 127-129);
    • escribir los mensajes de error encapsulados en la excepción [Throwable th] (líneas 124, 132);
  • líneas 93-120: el método [doFinally(ResultSet rs, PreparedStatement ps, Connection connexion)] gestiona la rama [finally] de los métodos de acceso a SGBD. Su objetivo es liberar los recursos utilizados por la conexión;

3.4.4. Eliminación del contenido de la tabla de productos

El método [delete] elimina el contenido de la tabla:


    // eliminación de productos
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // inicio de sesión
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // inicio de transacción
            connexion.setAutoCommit(false);
            // en modo lectura/escritura
            connexion.setReadOnly(false);
            // se vacía la tabla [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
            ps.executeUpdate();
            // confirmación de la transacción
            connexion.commit();
        } catch (SQLException e1) {
            // se gestiona la excepción
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // se procesa el finally
            doFinally(null, ps, connexion);
        }
}

La línea 7 utiliza las siguientes constantes de la clase [ConfigJdbc]:


public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "";

En la línea 13, la orden SQL preparada es la siguiente:


public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";

El método [delete] utiliza transacciones. Una transacción permite agrupar órdenes SQL que deben completarse todas con éxito o cancelarse todas. Hay cuatro operaciones que hay que conocer:

  • inicio de una transacción: [connexion.setAutoCommit(false)];
  • fin de una transacción con éxito: [connexion.commit()]. En este caso, se validan todas las operaciones realizadas en BD durante la transacción;
  • fin de una transacción fallida: [connexion.rollback()]. En este caso, se anulan todas las operaciones realizadas en BD durante la transacción;

En nuestros ejemplos, cada vez que se produce una excepción, anulamos la transacción en el método [doCatchException]:


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // se muestran los mensajes de error
        Static.show(title, Static.getErreursFromThrowable(th));
        // anulación de la transacción
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // se muestran los mensajes de error
            Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
        }
}

3.4.5. Creación del contenido de la tabla de productos

El método [insert] crea el contenido de la tabla:


public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // apertura de conexión
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // Inicio de la transacción
            connexion.setAutoCommit(false);
            // en modo lectura/escritura
            connexion.setReadOnly(false);
            // se rellena la tabla
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
            for (int i = 0; i < 10; i++) {
                // preparación
                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));
                // ejecución
                ps.executeUpdate();
            }
            // confirmación de la transacción
            connexion.commit();
        } catch (SQLException e1) {
            // se gestiona la excepción
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // se procesa el finally
            doFinally(null, ps, connexion);
        }
    }

Línea 12, la orden SQL preparada es la siguiente:


public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";

3.4.6. Visualización del contenido de la tabla de productos

El método [select] muestra el contenido de la tabla:


// lista de productos
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // apertura de conexión
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // inicio de la transacción
            connexion.setAutoCommit(false);
            // en modo de solo lectura
            connexion.setReadOnly(true);
            // se lee la tabla [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // confirmación de la transacción
            connexion.commit();
        } catch (SQLException e1) {
            // se gestiona la excepción
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // se procesa el finally
            doFinally(rs, ps, connexion);
        }
    }

Línea 14, el comando SQL preparado es el siguiente:


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

El método [affiche] (línea 18) es el siguiente:


    // visualización jSON de un objeto
    private static void affiche(Object object) {
        try {
            System.out.println(jsonMapper.writeValueAsString(object));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

Muestra la representación jSON del objeto pasado como parámetro (véase jSON, apartado 23.12).

3.4.7. Actualización del contenido de la tabla

El método [update] actualiza determinados productos:


    // actualización de determinados productos
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // apertura de sesión
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // Inicio de transacción
            connexion.setAutoCommit(false);
            // en modo lectura/escritura
            connexion.setReadOnly(false);
            // se actualiza la tabla
            ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
            // categoría 1
            ps.setInt(1, 1);
            // ejecución
            ps.executeUpdate();
            // confirmación de la transacción
            connexion.commit();
        } catch (SQLException e1) {
            // se gestiona la excepción
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // se procesa el finally
            doFinally(null, ps, connexion);
        }
}

Línea 13, la orden SQL preparada es la siguiente:


public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";

3.4.8. Función de la transacción

El método [insert2] inserta dos productos con la misma clave primaria en la tabla, lo cual no es posible. Como estamos en una transacción, la primera inserción se anulará.


    // se añaden 2 productos con las mismas claves primarias
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // apertura de conexión
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // inicio de la transacción
            connexion.setAutoCommit(false);
            // en modo lectura/escritura
            connexion.setReadOnly(false);
            // se añade 1 línea
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
            // ejecución
            ps.executeUpdate();
            // se añade la misma línea por segunda vez, por lo tanto con la misma clave primaria
            // INSERTion debe fallar y ninguno de los dos elementos debe insertarse debido a la transacción
            ps.executeUpdate();
            // confirmar transacción
            connexion.commit();
        } catch (SQLException e1) {
            // se gestiona la excepción
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
                    connexion, e1);
        } finally {
            // se procesa el finally
            doFinally(null, ps, connexion);
        }
}

Línea 13, la orden SQL preparada es la siguiente:


public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

3.4.9. Resultados

Se ejecuta la configuración de ejecución denominada [spring-jdbc-generic-01.IntroJdbc01]:

 

Se obtienen los siguientes resultados en la consola:


------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire : 
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Travail terminé
  • línea 30: antes de insertar los dos productos con la misma clave primaria, la tabla está vacía;
  • línea 35: tras la inserción de los dos productos con la misma clave primaria, la tabla está vacía. Esto demuestra la función de la transacción:
    • la primera inserción se realiza correctamente. No hay motivo para que falle;
    • la segunda inserción falla (línea 32). Por lo tanto, dado que estas dos inserciones se encuentran dentro de la misma transacción, todas las órdenes SQL de esta se anulan, incluida la primera inserción.

3.4.10. Conclusión

Lo que llama la atención en los códigos anteriores es el gran espacio que ocupa la gestión de la excepción [SQLException]. Dado que cualquier operación JDBC puede lanzarla, hay numerosos try / catch en el código.

3.5. Ejemplo-02

Retomaremos la aplicación anterior utilizando una fuente de datos de tipo [javax.sql.DataSource]:

Image

Vamos a utilizar una fuente de datos implementada por la clase [org.apache.tomcat.jdbc.pool.DataSource]. Esta clase utiliza un grupo de conexiones, es decir, un conjunto de conexiones abiertas:

  • cuando se instancia el grupo, se abre un número determinado de conexiones con la base de datos. Este número es configurable;
  • cuando el código Java abre una conexión, esta es proporcionada por el grupo;
  • cuando el código Java cierra una conexión, esta se devuelve al grupo;

Al final, las conexiones solo se abren una vez, lo que mejora el rendimiento del acceso a la base de datos. La fuente de datos se definirá en una clase de configuración de Spring

3.5.1. La arquitectura del proyecto

En este ejemplo, un programa de consola utiliza la interfaz de la capa [JDBC].

3.5.2. El proyecto Eclipse

El nuevo proyecto Eclipse se puede obtener copiando el anterior [1-6]:

A continuación, se actualiza el proyecto de [6] a [7]:

3.5.3. Configuración de Maven

El proyecto [7] es un proyecto Maven definido por el siguiente archivo [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-02</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- búsqueda del padre en el repositorio -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- configuración JDBC del SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • líneas 28-33: la dependencia de Maven del proyecto [mysql-config-jdbc];

Es el proyecto [mysql-config-jdbc] el que incluye en sus dependencias de Maven la biblioteca que proporciona una implementación de una fuente de datos de tipo [javax.sql.DataSource] (véase el apartado 3.3.2):


        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
</dependency>

3.5.4. Configuración de Spring

  

La clase de configuración de Spring [AppConfig] es la siguiente:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {
    // fuente de datos
    @Bean
    public DataSource dataSource() {
        // fuente de datos TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuración de acceso JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // conexiones abiertas inicialmente
        dataSource.setInitialSize(5);
        // resultado
        return dataSource;
    }

}
  • línea 10: [AppConfig] es una clase de configuración de Spring;
  • línea 11: importación de la clase de configuración [generic.jdbc.config.ConfigJdbc.class] definida en el proyecto [mysql-config-jdbc]. Esto significa que se dispone de todos los beans definidos por este archivo de configuración;
  • líneas 14-27: el bean de Spring que define la fuente de datos;
  • línea 17: creación de la fuente de datos, aún sin configurar;
  • líneas 19-22: la información que permite a la fuente de datos conectarse a la base de datos;
  • línea 24: crea un grupo de 5 conexiones. Aquí solo necesitamos una. Nunca hay varias conexiones simultáneas;

3.5.5. La clase principal

La clase principal [IntroJdbc02] es la siguiente:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class IntroJdbc02 {

    // asignar jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
    // fuente de datos
    private static DataSource dataSource;

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = null;
        try {
            // recuperación del contexto Spring
            ctx = new AnnotationConfigApplicationContext(AppConfig.class);
            // recuperación de la fuente de datos
            dataSource = ctx.getBean(DataSource.class);
            // se vacía la tabla [PRODUITS]
            System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
            delete();
...
        // fin
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // lista de productos
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // apertura de sesión
            connexion = dataSource.getConnection();
            // Inicio de transacción
            connexion.setAutoCommit(false);
            // en modo de solo lectura
            connexion.setReadOnly(true);
            // se lee la tabla [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // confirmar transacción
            connexion.commit();
        } catch (SQLException e1) {
            // se gestiona la excepción
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // se procesa el finally
            doFinally(rs, ps, connexion);
        }
    }
...
  • línea 25: la fuente de datos. Cabe señalar que es de tipo [javax.sql.DataSource] (línea 13), que es una interfaz;
  • línea 31: instanciación de los objetos Spring;
  • línea 32: obtención de una referencia a la fuente de datos. Cabe señalar que en ningún momento se menciona la clase realmente utilizada. Por lo tanto, aquí nada sugiere que se utilice una implementación [TomcatJdbc];
  • línea 49: obtención de una conexión abierta. Así es como los diferentes métodos de [IntroJdbc02] obtienen una conexión con la base de datos. El resto del código es idéntico al de la clase [IntroJdbc01];

3.5.6. Las pruebas

Se ejecuta la configuración de ejecución denominada [spring-jdbc-generic-02.IntroJdbc02]:

 

Se obtienen los mismos resultados que anteriormente (apartado 3.4.9).

3.6. Ejemplo-03

3.6.1. La arquitectura del proyecto

En este ejemplo, los métodos de acceso a los datos se aíslan en una capa [dao]. Se someterán a prueba mediante una prueba JUnit.

3.6.2. El proyecto Eclipse

El proyecto Eclipse [spring-jdbc-03] es un proyecto Spring/Maven construido como el anterior y completado de la siguiente manera:

 

Los diferentes paquetes tienen las siguientes funciones:

  • [spring.jdbc.config]: configuración del proyecto Spring;
  • [spring.jdbc.dao]: implementación de la capa [DAO];
  • [spring.jdbc.infrastructure]: implementa la excepción no controlada [DaoException];

3.6.3. Configuración de Maven

El proyecto Maven se configura mediante el siguiente archivo [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-03</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-03</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- búsqueda del padre en el repositorio -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- configuración JDBC del SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>

Es idéntico al del proyecto [spring-jdbc-02]. En particular, utiliza la dependencia Maven del proyecto [mysql-config-jdbc] (líneas 28-32).

3.6.4. Interfaz de la capa [DAO]

  

La capa [DAO] presenta la siguiente interfaz [IDao]:


package spring.jdbc.dao;

import java.util.List;

import spring.jdbc.entities.Produit;

public interface IDao {

    // Añadir productos
    public List<Produit> addProduits(List<Produit> produits);

    // lista de todos los productos
    public List<Produit> getAllProduits();

    // un producto concreto
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // actualización de varios productos
    public int updateProduits(List<Produit> produits);

    // eliminar todos los productos
    public int deleteAllProduits();

    // eliminar varios productos
    public int deleteProduits(int[] ids);
}

3.6.5. La clase [DaoException]

La clase [DaoException] se limita a extender la clase [UncheckedException] presentada en el apartado 3.3.5:

  

package spring.jdbc.infrastructure;

public class DaoException extends UncheckedException {

    private static final long serialVersionUID = 1L;

    // fabricantes
    public DaoException() {
        super();
    }

    public DaoException(int code, Throwable e, String className) {
        super(code, e, className);
    }

}

3.6.6. Configuración del proyecto Spring

  

La clase [AppConfig] que configura el proyecto Spring es idéntica al archivo de configuración de Spring del ejemplo [spring-jdbc-02], salvo por la línea 11:


package spring.jdbc.config;

import generic.jdbc.config.ConfigJdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
public class AppConfig {
    // fuente de datos
    @Bean
    public DataSource dataSource() {
        // fuente de datos TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuración de acceso JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // conexiones abiertas inicialmente
        dataSource.setInitialSize(5);
        // resultado
        return dataSource;
    }
}
  • línea 11: se analizará el paquete [spring.jdbc.dao] para encontrar otros componentes Spring distintos de los definidos en este archivo de configuración;

3.6.7. Implementación de la capa [DAO]

  

Recordemos (apartado 3.6.4) que la capa [DAO] implementa la siguiente interfaz [IDao]:


package spring.jdbc.dao;

import generic.jdbc.entities.dbproduits.Produit;

import java.util.List;

public interface IDao {

    // añadir productos
    public List<Produit> addProduits(List<Produit> produits);

    // lista de todos los productos
    public List<Produit> getAllProduits();

    // un producto concreto
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // actualización de varios productos
    public int updateProduits(List<Produit> produits);

    // eliminar todos los productos
    public int deleteAllProduits();

    // eliminación de varios productos
    public int deleteProduits(int[] ids);
}

Las clases [Dao1, Dao2] implementan ambas esta interfaz. La clase [Dao2] es una variante de la clase [Dao1] que introduce una novedad sintáctica. Nos centraremos en la clase [Dao1]. Su estructura básica es la siguiente:


package spring.jdbc.dao;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import spring.jdbc.infrastructure.DaoException;

@Component("dao1")
public class Dao1 implements IDao {

    // nombre de la clase
    private String simpleClassName = getClass().getSimpleName();
    // fuente de datos
    @Autowired
    protected DataSource dataSource;

    // fabricante
    public Dao1() {
        System.out.println("building Dao1...");
    }

    // ------------------------------- interfaz
    @Override
    public List<Produit> getAllProduits() {
...
    }

    @Override
    public Produit getProduitById(int id) {
...
    }

    @Override
    public Produit getProduitByName(String name) {
...
    }

    @Override
    public List<Produit> addProduits(List<Produit> produits) {
....
    }

    @Override
    public int updateProduits(List<Produit> produits) {
...
    }

    @Override
    public int deleteAllProduits() {
...
    }

    @Override
    public int deleteProduits(int[] ids) {
...
    }

    // ---------------------------------------- métodos locales
    // gestión finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        ...
    }

    // gestión de catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code, DaoException daoException) {
...
}
  • línea 20: la clase [Dao] es un componente Spring denominado [dao1]. Este nombre es opcional. Cuando no está presente, se utiliza el nombre de la clase con la primera letra mayúscula convertida en minúscula;
  • línea 24: el nombre de la clase. Se evita escribir [Dao] de forma fija para dejar abierta la posibilidad de renombrar la clase sin tener que redefinir este campo, que así sigue siendo válido;
  • líneas 26-27: inyección de la fuente de datos [tomcat-jdbc] definida en la clase de configuración [AppConfig];
  • líneas 36-68: implementación de la interfaz [IDao];
  • líneas 78-80: gestión centralizada del catch de los diferentes métodos;
  • líneas 72-75: gestión centralizada del finally de los distintos métodos;

El bloque catch de los distintos métodos se gestiona de la siguiente manera:


    // gestión de catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
        // anulación de transacción
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            e2.printStackTrace();
        }
        // daoException
        return new DaoException(code, th, simpleClassName);
}
  • línea 2: el método se declara como [protected], lo que permite a las clases hijas utilizarlo sin que sea público. Recibe los siguientes parámetros:
    • [Connection connexion]: la conexión con SGBD; puede ser nula;
    • [Throwable th]: la excepción que se ha producido y que se va a encapsular en un tipo [DaoException];
    • [int code]: un código de error que se utilizará si el método crea un nuevo [DaoException];
  • líneas 4-7: la función principal de este método es cancelar la transacción vinculada a la conexión pasada como parámetro 1;
  • líneas 8-10: si la cancelación de la transacción ha fallado, se escribe el rastro de la excepción en la consola. No se puede hacer mucho más, ya que se va a lanzar una excepción en la línea 12;

El finally de los distintos métodos se gestiona de la siguiente manera:


// gestión finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        // cierre ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

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

            }
        }
        // cierre de sesión
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // se registra el error si es posible
                if (daoException == null) {
                    daoException = new DaoException(code, e3, simpleClassName);
                }
            }
        }
        // resultado
        return daoException;
    }
  • línea 2: este método también se declara como [protected]. Recibe los siguientes parámetros:
    • [ResultSet rs]: el posible [ResultSet] si se ha ejecutado una operación [SELECT]; puede ser nulo;
    • [PreparedStatement ps]: el [PreparedStatement] que se ha ejecutado; puede ser nulo;
    • [Connection connexion]: la conexión con el SGBD; puede ser nulo;
    • [int code]: un código de error que se utilizará si el método crea un nuevo [DaoException];
    • [DaoException daoException]: el posible [DaoException] si se ha producido una excepción antes del finally; puede ser nulo;
  • líneas 21-30: el objetivo principal de este método es cerrar la conexión (línea 23);
  • líneas 24-29: si durante este cierre se produce una excepción, se comprueba el estado del parámetro [DaoException daoException] que se nos ha pasado: si es [daoException == null], se crea un nuevo [DaoException] con el código pasado como parámetro;
  • línea 32: se devuelve como resultado el antiguo o el nuevo [DaoException];

No vamos a presentar todos los métodos de la clase [Dao], sino solo algunos. Todos son muy similares.

3.6.7.1. El método [getProduitById]

El método [getProduitById] devuelve el producto cuya clave primaria es igual al parámetro [id] o null en caso contrario;


@Override
    public Produit getProduitById(int id) {
        // recursos de la conexión
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        // al principio no hay excepción
        DaoException daoException = null;
        // el producto buscado
        Produit produit = null;
        try {
            // apertura de la conexión
            connexion = dataSource.getConnection();
            // inicio de la transacción
            connexion.setAutoCommit(false);
            // en modo de solo lectura
            connexion.setReadOnly(true);
            // se lee la tabla [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_PRODUIT_BYID);
            ps.setInt(1, id);
            rs = ps.executeQuery();
            if (rs.next()) {
                produit = new Produit(id, rs.getString(1), rs.getInt(2), rs.getDouble(3), rs.getString(4));
            }
            // confirmación de transacción
            connexion.commit();
            // vuelta al modo predeterminado
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // se gestiona la excepción
            daoException = doCatchException(connexion, e1, 112);
        } finally {
            // se procesa el finally
            daoException = doFinally(rs, ps, connexion, 113, daoException);
        }
        // ¿excepción?
        if (daoException != null) {
            throw daoException;
        }
        // resultado
        return produit;
    }
  • línea 10: el producto que se va a devolver se establece en nulo;
  • línea 19: la orden SQL [ConfigJdbc.V2_SELECT_PRODUIT_BYID] es la siguiente:

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

  • líneas 22-24: si [ResultSet] tiene una línea, se utiliza para crear el producto a devolver; de lo contrario, el producto a devolver sigue siendo nulo;
  • línea 41: se devuelve el producto;
  • línea 8: la excepción [DaoException] del método se inicializa a null;
  • línea 31: el método [doCatchException] crea una excepción [DaoException];
  • línea 34: el parámetro [daoException] del método [doFinally] es o bien null, o bien la excepción creada por el método [doCatchException]. El método [doFinally]:
    • deja este parámetro tal cual si consigue cerrar la conexión;
    • deja este parámetro tal cual si no consigue cerrar la conexión y ya ha habido un [DaoException] anteriormente;
    • crea un nuevo [DaoException] si no consigue cerrar la conexión y no ha habido un [DaoException] anteriormente;
  • líneas 37-39: si la excepción local [daoException] no es nula, se lanza; de lo contrario, se devuelve el resultado solicitado (línea 41);

3.6.7.2. El método [deleteProduits]

El método [deleteProduits] elimina los productos cuyas claves primarias se le pasan como parámetro. Devuelve el número de productos eliminados.


@Override
    public int deleteProduits(int[] ids) {
        // recursos de la conexión
        PreparedStatement ps = null;
        Connection connexion = null;
        // al principio no hay excepción
        DaoException daoException = null;
        // número de productos actualizados
        int nbProduits = 0;
        try {
            // apertura de conexión
            connexion = dataSource.getConnection();
            // inicio de la transacción
            connexion.setAutoCommit(false);
            // en modo lectura/escritura
            connexion.setReadOnly(false);
            // se eliminan los productos
            ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
            for (int id : ids) {
                // parámetros
                ps.setInt(1, id);
                // ejecución
                nbProduits += ps.executeUpdate();
            }
            // confirmar transacción
            connexion.commit();
            // volver al modo predeterminado
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // se gestiona la excepción
            daoException = doCatchException(connexion, e1, 171);
        } finally {
            // se procesa el finally
            daoException = doFinally(null, ps, connexion, 172, daoException);
        }
        // ¿excepción?
        if (daoException != null) {
            throw daoException;
        }
        // resultado
        return nbProduits;
    }
  • línea 18, la orden SQL [ConfigJdbc.V2_DELETE_PRODUITS] es la siguiente:

public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";

  • líneas 18-24: el código de eliminación de productos. Se observa que la orden SQL se prepara una vez (línea 18) y se ejecuta n veces (líneas 19-24). Este es el interés del objeto [PreparedStatement];
  • línea 23: el método [PreparedStatement].executeUpdate() devuelve el número de líneas afectadas por la operación de actualización;
  • línea 41: se devuelve el número de productos actualizados;

3.6.7.3. El método [updateProduits]

El método [updateProduits] actualiza en la base de datos los productos que se le pasan como parámetros. Devuelve el número de productos actualizados.


@Override
    public int updateProduits(List<Produit> produits) {
        // recursos de la conexión
        PreparedStatement ps = null;
        Connection connexion = null;
        // al principio no hay excepción
        DaoException daoException = null;
        // número de productos actualizados
        int nbProduits = 0;
        try {
            // apertura de conexión
            connexion = dataSource.getConnection();
            // inicio de la transacción
            connexion.setAutoCommit(false);
            // en modo lectura/escritura
            connexion.setReadOnly(false);
            // se actualiza la tabla [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
            for (Produit produit : produits) {
                // parámetros
                ps.setString(1, produit.getNom());
                ps.setDouble(2, produit.getPrix());
                ps.setInt(3, produit.getCategorie());
                ps.setString(4, produit.getDescription());
                ps.setInt(5, produit.getId());
                // ejecución
                nbProduits += ps.executeUpdate();
            }
            // confirmar transacción
            connexion.commit();
            // vuelta al modo predeterminado
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // se gestiona la excepción
            daoException = doCatchException(connexion, e1, 131);
        } finally {
            // se procesa el finally
            daoException = doFinally(null, ps, connexion, 132, daoException);
        }
        // ¿excepción?
        if (daoException != null) {
            throw daoException;
        }
        // resultado
        return nbProduits;
    }
  • línea 18: el orden SQL [ConfigJdbc.V2_UPDATE_PRODUITS] es el siguiente:

public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
  • líneas 19-28: el código de actualización de los productos;

3.6.7.4. El método [addProduits]

El método [addProduits] añade a la base de datos los productos que se le pasan como parámetros. Devuelve esos mismos productos con sus claves primarias (antes de introducirlos en la base de datos, los productos no tienen clave primaria).


@Override
    public List<Produit> addProduits(List<Produit> produits) {
        // recursos de la conexión
        PreparedStatement ps = null;
        Connection connexion = null;
        // al principio no hay excepción
        DaoException daoException = null;
        try {
            // apertura de la conexión
            connexion = dataSource.getConnection();
            // en modo lectura/escritura
            connexion.setReadOnly(false);
            // Inicio de la transacción
            connexion.setAutoCommit(false);
            // se añaden elementos a la tabla [PRODUITS]
            String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
            ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
            for (Produit produit : produits) {
                // parámetros
                ps.setString(1, produit.getNom());
                ps.setLong(2, produit.getCategorie());
                ps.setDouble(3, produit.getPrix());
                ps.setString(4, produit.getDescription());
                // ejecución de comando
                ps.executeUpdate();
                // clave primaria generada
                ResultSet generatedKeys = ps.getGeneratedKeys();
                if (generatedKeys.next()) {
                    produit.setId(generatedKeys.getInt(1));
                } else {
                    throw new RuntimeException(String.format("Le produit de nom [%s] n'a pas récupéré de clé primaire",
                            produit.getNom()));
                }
            }
            // confirmar transacción
            connexion.commit();
            // vuelta al modo predeterminado
            connexion.setAutoCommit(true);
        } catch (SQLException | RuntimeException e1) {
            // se procesa la excepción
            daoException = doCatchException(connexion, e1, 151);
        } finally {
            // se procesa el finally
            daoException = doFinally(null, ps, connexion, 152, daoException);
        }
        // ¿excepción?
        if (daoException != null) {
            throw daoException;
        }
        // resultado
        return produits;
}
  • línea 16, la orden SQL [ConfigJdbc.V2_INSERT_PRODUITS] es la siguiente:

public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";

En el ejemplo anterior, el comando de inserción de un producto no incluye la clave primaria [ID]. Dado que la clave primaria de la base MySQL tiene el atributo [AUTOINCREMENT], SGBD generará entonces una clave primaria para cada inserción. Se plantea el problema de cómo recuperarla. Es un punto importante, ya que las operaciones sobre los productos se realizan a través de sus claves primarias. Por lo tanto, es necesario conocerlas;

  • líneas 17-33: el bucle de inserción de productos;
  • línea 16: una forma particular del método [prepareStatement]. El segundo parámetro [generatedColumns] es una matriz de nombres de columnas cuyos valores queremos recuperar tras la inserción. En la línea 16, hemos indicado que queríamos recuperar el valor de la columna [id]. Cabe señalar aquí que, aunque el nombre de las columnas de una tabla no distingue entre mayúsculas y minúsculas, el SGBD PostgreSQL ha querido que este nombre esté en minúsculas. Este es el tipo de problema que suele surgir al portar código de un SGBD a otro;
  • línea 24: inserción de una línea en la base de datos;
  • línea 26: se recupera la lista de valores de las columnas especificadas en la línea 16 de un [ResultSet]. Aquí, para 1 inserción, el [ResultSet] tendrá 1 línea y esta línea tendrá una única columna que contiene la clave primaria;
  • línea 28: se recupera la clave primaria generada por el SGBD;
  • líneas 29-32: si no se obtiene la clave primaria generada, se lanza un [RuntimeException] que se encapsulará en un [DaoException] líneas 38-40;

3.6.8. La clase [Dao2]

  

La clase [Dao2] es una variante de la clase [Dao1] que utiliza una sintaxis denominada try-with-resource(resource):

1
2
3
4
try(resource){
...
}
...
  • [resource] es un recurso que implementa la interfaz [java.lang.AutoCloseable]. Todos los recursos que se liberan con el método [close] forman parte de ella. Esta sintaxis garantiza que, en la línea 4, se cerrará el recurso [resource]. Esto evita tener que escribir una cláusula [finally] para realizar esta tarea de cierre;

Tomemos como ejemplo el método [getAllProduits] de la clase [Dao2]:


    @Override
    public List<Produit> getAllProduits() {
        // posible excepción
        DaoException daoException = null;
        // lista de productos
        List<Produit> produits = new ArrayList<Produit>();
        try (Connection connexion = dataSource.getConnection()) {
            // inicio de la transacción
            connexion.setAutoCommit(false);
            // en modo de solo lectura
            connexion.setReadOnly(true);
            // se lee la tabla [PRODUITS]
            try (PreparedStatement ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_ALLPRODUITS)) {
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        produits.add(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
                    }
                }
                // fin de la transacción
                connexion.commit();
                // vuelta al modo predeterminado
                connexion.setAutoCommit(true);
            } catch (SQLException e1) {
                // se cancela la transacción
                daoException = doRollback(connexion, e1, 111);
            }
        } catch (SQLException e2) {
            // se procesa la excepción
            if (daoException == null) {
                daoException = new DaoException(112, e2, simpleClassName);
            }
        }
        // ¿excepción?
        if (daoException != null) {
            throw daoException;
        }
        // resultado
        return produits;
}
  • línea 7: try con el recurso [Connection]. En la línea 27, se garantiza que este está cerrado;
  • línea 13: try con el recurso [PreparedStatement]. En la línea 23, se garantiza que este está cerrado;
  • línea 14: try con el recurso [ResultSet]. Línea 19, se garantiza que este está cerrado;
  • línea 25: la transacción se cancela de la siguiente manera:

    private DaoException doRollback(Connection connexion, Throwable e1, int code) {
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // génération de l'exception
        return new DaoException(code, e1, simpleClassName);
}

Al final, tenemos un código más fácil de leer.

3.6.9. Implementación de la capa de pruebas

3.6.9.1. Las clases de prueba

  
  • la prueba [JUnitTestDao1] es una prueba JUnit de la clase [Dao1];
  • la prueba [JUnitTestDao2] es una prueba JUnit de la clase [Dao2];
  • [AbstractJUnitTestDao] es la clase principal de las dos clases de prueba anteriores;
  • [MainTestDao1] es una clase de consola de prueba de la clase [Dao1];
  • [MainTestDao2] es una clase de consola de prueba de la clase [Dao2];
  • [AbstractMainTestDao] es la clase padre de las dos clases anteriores. Recoge el código de las clases de consola [IntroJdbc01, IntroJdbc02] ya presentadas, por lo que no estudiaremos estas clases de consola;

La clase [JUnitTestDao1] es la siguiente:


package spring.jdbc.tests;

import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;

@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao1 extends AbstractJUnitTestDao {

    // capa [DAO]
    @Autowired
    @Qualifier("dao1")
    private IDao dao;

    @Override
    IDao getDao() {
        return dao;
    }

}
  • Las anotaciones de las líneas 12-13 se presentaron en el apartado 2.5.5. Permiten que una prueba Junit tenga un acceso sencillo al contexto Spring y sus beans. Este contexto está configurado por la clase [AppConfig] (línea 12) estudiada en el apartado 2.4.3;
  • línea 14: la clase hereda de la clase [AbstractJUnitTestDao], que vamos a presentar. En esta clase se encuentran los métodos de prueba JUnit;
  • líneas 17-19: el bean denominado [dao1] (línea 18) se inyecta (línea 17). Por lo tanto, lo que se inyecta aquí es una instancia de la clase [Dao1];
  • líneas 21-24: el método [getDao] redefine el método del mismo nombre en la clase padre;

En definitiva, el objetivo de esta clase es devolver a la clase padre una referencia a la capa [DAO] que debe ser probada, en este caso una instancia de [Dao1]. Del mismo modo, la clase [JUnitTestDao2] devuelve a la clase padre [AbstractJUnitTestDao] una instancia de la clase [Dao2].

La clase [AbstractJUnitTestDao] es una clase de pruebas de JUnit:


package spring.jdbc.tests;

import generic.jdbc.entities.dbproduits.Produit;

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

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.BeansException;

import spring.jdbc.dao.IDao;
import spring.jdbc.infrastructure.DaoException;

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

public abstract class AbstractJUnitTestDao {

    // capa [DAO]
    abstract IDao getDao();

    // asignar jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();

    @Before
    public void clean() {
        // se limpia la base antes de cada prueba
        log("Vidage de la base de données", 1);
        getDao().deleteAllProduits();
    }

    @Test
    public void getProduits() throws JsonProcessingException {
    ...
    }

    @Test
    public void getProduitBy() {
    ...
    }

    @Test
    public void doInsertsInTransaction() {
...
    }

    @Test
    public void updateProduits() {
    ...
    }

    @Test
    public void deleteProduits() {
    ....
    }

    @Test
    public void perf1() {
        ...
    }

    @Test
    public void perf2() {
    ...
    }

    @Test
    public void perf3() {
    ....
    }

    // -------------- métodos privados
...
}
  • línea 19, la clase [AbstractJUnitTestDao] es abstracta;
  • línea 22: el método abstracto [getDao] que permite obtener la referencia de la capa [DAO] que se va a probar. Este método es implementado por las clases hijas;
  • línea 25: un mapeador jSON que nos permitirá mostrar en la consola el valor jSON de los productos;
  • líneas 27-32: antes de cada prueba (línea 27), se vacía la tabla [PRODUITS];

3.6.9.2. El método privado [fill]

El método privado [fill] se utiliza para introducir productos en la tabla [PRODUITS].


private List<Produit> fill(int nbProduits) {
        log("Remplissage de la base de données", 1);
        // on crée une liste de produits
        List<Produit> produits = new ArrayList<Produit>();
        for (int i = 0; i < nbProduits; i++) {
            int n = i + 1;
            // int id, String nom, int categorie, double prix, String description
            produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
                    "DESC%s", n)));
        }
        // on la persiste en base - on récupère des produits avec leur clé primaire
        produits = getDao().addProduits(produits);
        // on crée un dictionnaire des produits pour pouvoir les retrouver + facilement
        // la clé du dictionnaire est la clé primaire du produit en base
        for (Produit produit : produits) {
            mapProduits.put(produit.getId(), produit);
        }
        // on rend les produits
        return produits;
    }
  • línea 1: el método [fill] inserta [nbProduits] en la tabla [PRODUITS], que se supone vacía;
  • líneas 3-10: creación de una lista de productos con el formato:

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

que utiliza el constructor Producto(int id, String nombre, int categoría, doble precio, String descripción). El valor del primer parámetro [id] (clave primaria de la tabla [PRODUITS]) no tiene importancia, ya que el método [addProduits] de la línea 10 no lo inserta en la base de datos y deja que SGBD genere su valor;

  • línea 12: la lista de productos se guarda en la base de datos. Cada uno de los productos de esta lista se enriquece con una nueva clave primaria [id]. El método [addProduits] devuelve como resultado su parámetro [produits]. Por lo tanto, podríamos haber omitido la recuperación del resultado;
  • líneas 15-17: se colocan los productos en un diccionario:

    // diccionario de productos
    private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();

La clave del diccionario es la clave primaria del producto y el valor asociado, el propio producto;

  • línea 19: devolvemos la lista de productos;

3.6.9.3. La prueba [getProduits]

Esta es la siguiente:


    @Test
    public void getProduits() throws JsonProcessingException {
        // remplissage
        fill(10);
        // liste des produits
        log("Liste des produits", 2);
        List<Produit> produits = getDao().getAllProduits();
        affiche(produits);
        // on vérifie que la liste récupérée et celle persistée sont les mêmes
        for (Produit produit : produits) {
            Produit found = mapProduits.get(produit.getId());
            Assert.assertEquals(found, produit);
            mapProduits.remove(found.getId());
        }
        // tous les produits initiaux doivent avoir disparu du dictionnaire
        Assert.assertEquals(0, mapProduits.size());
}
}
  • línea 4: se introducen 10 productos en la base de datos;
  • línea 7: una vez hecho esto, se solicita ver todos los productos de la base;
  • línea 8: se muestran. El objetivo es comprobar que los productos se han registrado correctamente y que tienen una clave primaria;
  • líneas 10-13: se comprueba que los productos encontrados son idénticos a los que se han guardado y que se pueden encontrar en el diccionario [mapProduits];
  • línea 11: se recupera del diccionario el producto que tiene la misma clave primaria que el obtenido de la base de datos. Esto demuestra que los productos guardados han recuperado correctamente una clave primaria;
  • línea 12: se comprueba que los dos productos sean idénticos. Recordemos que la clase [Produit] ha definido un método [equals] (véase el apartado 3.3.4);
  • línea 13: se elimina del diccionario el elemento encontrado;
  • línea 16: se comprueba que el diccionario de productos iniciales está vacío, lo que significa que todos esos productos iniciales estaban presentes en la lista de productos recuperados de la base de datos;

El método [affiche] de la línea 8 es el siguiente método privado:


    // visualización de la lista de productos
    private <T> void affiche(List<T> elements) throws JsonProcessingException {
        for (T element : elements) {
            System.out.println(jsonMapper.writeValueAsString(element));
        }
}
  • línea 2: el método [affiche] es un método genérico. Está parametrizado por un tipo T, denotado sintácticamente como <T>. Si estuviera parametrizado por dos tipos, T1 y T2, se escribiría <T1,T2>. La sintaxis de un método m parametrizado por un tipo T es la siguiente:
portée <T> type_résultat m(... , T value1, ...){
...
    T value2=...
}

En el código del método m, encontraremos datos de tipo T. El método m de una instancia c de una clase C se puede llamar entonces de la siguiente manera:

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

donde T1 es el tipo efectivo que sustituirá al tipo formal T del método m. En la mayoría de los casos, el compilador es capaz de deducir el tipo T1 a partir de los argumentos del método m. Por lo tanto, la instrucción anterior se simplificará en la mayoría de los casos a:

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

Volvamos al método [affiche]. Este muestra una lista de elementos de tipo T. Esto es posible porque el mapeador jSON utilizado en la línea 4 es capaz de generar la representación jSON de cualquier tipo de objeto. En este ejemplo concreto, el único tipo T utilizado será el tipo [Produit].

El método [affiche] también podría haberse escrito de la siguiente manera:


    // visualización de la lista de productos
    private void affiche(Object o) throws JsonProcessingException {
            System.out.println(jsonMapper.writeValueAsString(o));
        }

Dado que el parámetro efectivo es una lista de productos, la línea 3 habría escrito la representación jSON de dicha lista. Esto no es lo mismo que escribir, uno por uno, la representación de cada uno de sus elementos.

La visualización generada por la prueba [getProduits] es la siguiente:

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

3.6.9.4. La prueba [getProduitBy]

Esta es la siguiente:


    @Test
    public void getProduitBy() {
        // relleno
        fill(10);
        log("getProduitBy", 1);
        Produit produit = getDao().getProduitByName("NOM3");
        Produit produit2 = getDao().getProduitById(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
}
  • línea 6: se utiliza el método [getProduitByName] de la interfaz [IDao] para recuperar el producto con nombre [NOM3];
  • línea 7: a continuación, se utiliza el método [getProduitById] de la interfaz [IDao] para recuperar el mismo producto, identificado esta vez por su clave primaria;
  • líneas 8-10: se comprueba que [produit2] y [produit] tienen las mismas características;

3.6.9.5. La prueba [doInsertsInTransaction]

Esta es la siguiente:


    @Test
    public void doInsertsInTransaction() {
        log("Ajout de deux produits de même nom", 1);
        // se realiza la inserción
        List<Produit> inserts = new ArrayList<Produit>();
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        boolean erreur = false;
        try {
            getDao().addProduits(inserts);
        } catch (DaoException daoException) {
            erreur = true;
        }
        // comprobaciones
        Assert.assertTrue(erreur);
        List<Produit> produits = getDao().getAllProduits();
        Assert.assertEquals(0, produits.size());
}
  • líneas 5-7: se crea una lista de dos productos con el mismo nombre [x];
  • línea 10: estos dos productos se insertan en la tabla [PRODUITS], que está vacía (método [clean] anotado con [@Before]). La primera inserción se realizará, pero no la segunda, ya que la tabla [PRODUITS] tiene una restricción de unicidad sobre el nombre de los productos. Por lo tanto, debe producirse una excepción. Esta se comprueba en la línea 15;
  • dado que todos los métodos de la interfaz [IDao] se ejecutan dentro de una transacción, el hecho de que la segunda inserción falle provocará la anulación de toda la transacción y, por lo tanto, de la primera inserción. Al final, no debe realizarse ninguna inserción en la tabla [PRODUITS];
  • líneas 16-17: se comprueba este punto solicitando la lista de productos contenidos en la tabla [PRODUITS] y verificando que dicha lista está vacía;

3.6.9.6. La prueba [updateProduits]

Esta es la siguiente:


    @Test
    public void updateProduits() {
        // remplissage
        fill(10);
        log("Mise à jour du prix des produits de catégorie 1", 1);
        // on récupère les produits
        List<Produit> produits = getDao().getAllProduits();
        // on met à jour ceux de catégorie 1
        List<Produit> updated = new ArrayList<Produit>();
        int nbUpdated = 0;
        for (Produit produit : produits) {
            if (produit.getCategorie() == 1) {
                // int id, String nom, int categorie, double prix, String description
                updated
                        .add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
                nbUpdated++;
            }
        }
        int nbProduits = getDao().updateProduits(updated);
        // vérifications
        // Assert.assertEquals(nbUpdated, nbProduits); -- no pasa con DB2
        for (Produit produit : updated) {
            Produit produit2 = getDao().getProduitById(produit.getId());
            Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
        }
}
  • línea 4: se introducen 10 productos en la base de datos;
  • línea 7: se recuperan;
  • líneas 9-18: se aumenta en un 10 % el precio de los productos de la categoría n.º 1;
  • línea 19: se guardan estos cambios en la base de datos;
  • líneas 22-25: se recorre en memoria la lista de productos que se ha utilizado para la actualización. Para cada uno de ellos, se busca en la base de datos el producto con la misma clave primaria y se comprueba que la actualización del precio se ha realizado correctamente;
  • línea 19: se recupera el número de productos actualizados por la operación [updateProduits];
  • línea 21: se comprueba que este número sea el esperado. Esta prueba se supera para todos los SGBD excepto para el SGBD y el DB2. Por lo tanto, se ha comentado;

3.6.9.7. La prueba [deleteProduits]

Esta es la siguiente:


    @Test
    public void deleteProduits() {
        // relleno
        fill(10);
        log("deleteProduits", 1);
        // lista de productos
        List<Produit> produits = getDao().getAllProduits();
        // eliminación de dos productos
        Produit produit0 = produits.get(0);
        Produit produit5 = produits.get(5);
        int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
        // comprobaciones
        // Assert.assertEquals(2, nbDeleted); -- no funciona con DB2
        Assert.assertNull(getDao().getProduitById(produit0.getId()));
        Assert.assertNull(getDao().getProduitById(produit5.getId()));
        Assert.assertEquals(produits.size() - 2, getDao().getAllProduits().size());
}
  • línea 4: se introducen 10 productos en la base de datos;
  • líneas 7-11: recuperamos todos los productos de la base de datos y eliminamos de ella los productos recuperados en las posiciones 0 y 5;
  • líneas 14-16: se comprueba que los dos productos ya no están en la base de datos y que esta tiene dos productos menos;
  • la prueba de la línea 13 no pasa con el SGBD DB2. Pasa con los demás SGBD;

3.6.9.8. Las pruebas de rendimiento

Se han incluido en las pruebas tres métodos cuyo único objetivo es evaluar el rendimiento del SGBD:


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

    @Test
    public void perf2() {
        // remplissage
        fill(10000);
        // modification
        List<Produit> produits = getDao().getAllProduits();
        // on met à jour ceux de catégorie 1
        List<Produit> updated = new ArrayList<Produit>();
        for (Produit produit : produits) {
            // int id, String nom, int categorie, double prix, String description
            updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
        }
        getDao().updateProduits(updated);
    }

    @Test
    public void perf3() {
        // remplissage
        fill(10000);
        // suppression
        List<Produit> produits = getDao().getAllProduits();
        // clés primaires
        int[] keys = new int[produits.size()];
        for (int i = 0; i < keys.length; i++) {
            keys[i] = produits.get(i).getId();
        }
        getDao().deleteProduits(keys);
}
  • líneas 1-5: inserción de 10 000 productos;
  • líneas 8-20: inserción de 10 000 productos y posterior modificación de los mismos mediante sus claves primarias;
  • líneas 23-34: inserción de 10 000 productos y posterior eliminación de los mismos mediante sus claves primarias;

Para ejecutar las pruebas [JUnitTestDao1] y [JUnitTestDao2], se pueden utilizar las siguientes configuraciones de ejecución:

Los resultados de la prueba [JUnitTestDao1] son los siguientes:

En [1] se muestran los resultados de [JUnitTestDao1] y en [2] los de [JUnitTestDao2]. No hay diferencias significativas entre ellos. En [1]:

  • la prueba se ha superado;
  • la inserción de 10 000 productos tarda 3,15 segundos;
  • la inserción de 10 000 productos seguida de su modificación dura 4,80 segundos;
  • la inserción de 10 000 productos seguida de su eliminación dura 4,40 segundos;
  • por lo tanto, lo más costoso es la inserción;