• 15 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 12/15/20

Simplifier l'exécution de requêtes SQL avec Spring JDBC

Lorsqu'il s'agit d'interagir avec une base de données, l'exécution de requêtes SQL via JDBC n'est pas difficile. Mais il s'agit d'un travail assez fastidieux : il faut, entre autres, ouvrir une connexion, créer un Statement, exécuter la requête, boucler sur le ResultSet pour charger les résultats et enfin tout fermer proprement (même en cas d'erreur).

Spring peut alléger le travail du développeur en encapsulant tout le code de « tuyauterie », le développeur n'ayant plus qu'à implémenter les parties spécifiques :

Action

Spring

Développeur

Définir la DataSource

 

Ouvrir la connexion

 

Spécifier la requête SQL

 

Déclarer les paramètres et fournir leur valeur

 

Préparer et exécuter la requête (Statement)

 

Boucler, si besoin, sur les résultats (ResultSet)

 

Faire ce qu'il y a à faire à chaque itération

 

Fermer la connexion, le Statement et le ResultSet

 

Prendre en charge les transactions

 

Traiter les exceptions

 

Travail préparatoire

Ajout de la dépendance vers Spring JDBC

Le module Spring à utiliser pour l'exécution des requêtes SQL est Spring JDBC. Il faut donc ajouter cette dépendance dans le projet Maven (module ticket-consumer) :

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
</dependency>

Et c'est tout !

Requête SQL de sélection

Définir et exécuter une requête SQL de sélection

Commençons par un exemple très simple : récupérer le nombre de tickets.

package org.example.demo.ticket.consumer.impl.dao;

import javax.inject.Named;
import org.example.demo.ticket.consumer.contract.dao.TicketDao;
import org.example.demo.ticket.model.recherche.ticket.RechercheTicket;
import org.springframework.jdbc.core.JdbcTemplate;


@Named
public class TicketDaoImpl extends AbstractDaoImpl implements TicketDao {

    @Override
    public int getCountTicket(RechercheTicket pRechercheTicket) {
        JdbcTemplate vJdbcTemplate = new JdbcTemplate(getDataSource());
        int vNbrTicket = vJdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM ticket",
            Integer.class);

        return vNbrTicket;
    }
}

Comparer à ce que vous devriez coder avec JDBC, avouez qu'avec Spring JDBC, c'est bien plus simple et rapide ! ;)

Je viens de vous montrer un premier élément de Spring JDBC : la classe JdbcTemplate. C'est grâce à cette classe, créée à partir d'une DataSource, que je définis et exécute une requête SQL.

Spécifier des paramètres

Ajoutons maintenant quelques paramètres : récupérer le nombre de tickets correspondant aux critères de recherche.

Les paramètres sont désignés par des ?, comme en JDBC classique.

public class TicketDaoImpl extends AbstractDaoImpl implements TicketDao {

    @Override
    public int getCountTicket(RechercheTicket pRechercheTicket) {
        String vSQL
            = "SELECT COUNT(*) FROM ticket"
            + " WHERE auteur_id = ?"
            + "   AND projet_id = ?";

        JdbcTemplate vJdbcTemplate = new JdbcTemplate(getDataSource());
        int vNbrTickets = vJdbcTemplate.queryForObject(
            vSQL, Integer.class,
            pRechercheTicket.getAuteurId(),
            pRechercheTicket.getProjetId());

        return vNbrTicket;
    }
}

Spring permet également d'utiliser des paramètres nommés (de la forme :param) grâce à la classe NamedParameterJdbcTemplate (utilisée à la place de JdbcTemplate) :

public class TicketDaoImpl extends AbstractDaoImpl implements TicketDao {

    @Override
    public int getCountTicket(RechercheTicket pRechercheTicket) {
        String vSQL
            = "SELECT COUNT(*) FROM ticket"
            + " WHERE auteur_id = :auteur_id"
            + "   AND projet_id = :projet_id";

        NamedParameterJdbcTemplate vJdbcTemplate = new NamedParameterJdbcTemplate(getDataSource());

        MapSqlParameterSource vParams = new MapSqlParameterSource();
        vParams.addValue("auteur_id", pRechercheTicket.getAuteurId());
        vParams.addValue("projet_id", pRechercheTicket.getProjetId());

        int vNbrTicket = vJdbcTemplate.queryForObject(vSQL, vParams, Integer.class);

        return vNbrTicket;
    }
}
public class TicketDaoImpl extends AbstractDaoImpl implements TicketDao {

    @Override
    public int getCountTicket(RechercheTicket pRechercheTicket) {
        MapSqlParameterSource vParams = new MapSqlParameterSource();

        StringBuilder vSQL = new StringBuilder("SELECT COUNT(*) FROM ticket WHERE 1=1");

        if (pRechercheTicket != null) {
            if (pRechercheTicket.getAuteurId() != null) {
                vSQL.append(" AND auteur_id = :auteur_id");
                vParams.addValue("auteur_id", pRechercheTicket.getAuteurId());
            }
            if (pRechercheTicket.getProjetId() != null) {
                vSQL.append(" AND projet_id = :projet_id");
                vParams.addValue("projet_id", pRechercheTicket.getProjetId());
            }
        }

        NamedParameterJdbcTemplate vJdbcTemplate = new NamedParameterJdbcTemplate(getDataSource());
        int vNbrTicket = vJdbcTemplate.queryForObject(vSQL.toString(), vParams, Integer.class);

        return vNbrTicket;
    }
}

Je reviendrai sur la spécification des paramètres dans la section traitant des requêtes de mise à jour.

Récupérer le résultat d'une requête

Jusqu'à présent j'ai abordé des exemples très simples où la requête ne retourne qu'une seule ligne avec une seule colonne. Spring permet de gérer très simplement ces cas en revoyant directement un objet Java correspondant à la valeur retournée par la requête SQL.

Abordons maintenant un cas plus intéressant : la requête renvoie plusieurs lignes et colonnes et je veux obtenir une liste d'objets.

Si vous reprenez les tableaux de répartition des responsabilités entre le développeur et Spring, vous voyez que le développeur n'a à coder que le travail à faire sur chaque ligne de résultat. C'est-à-dire qu'il doit simplement coder le mapping entre une ligne de résultat et l'objet Java correspondant.

Pour ce faire, le développeur doit implémenter une interface fournie par Spring : RowMapper<T>. Le nom est parlant : il s'agit de mapper une ligne de résultat en objet de type T.

Commençons pas un cas simple : charger tous les statuts de ticket depuis la base de données.

package org.example.demo.ticket.consumer.impl.dao;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import javax.inject.Named;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import org.example.demo.ticket.consumer.contract.dao.TicketDao;
import org.example.demo.ticket.model.bean.ticket.TicketStatut;


@Named
public class TicketDaoImpl extends AbstractDaoImpl implements TicketDao {
    // ...

    @Override
    public List<TicketStatut> getListStatut() {
        String vSQL = "SELECT * FROM public.statut";

        JdbcTemplate vJdbcTemplate = new JdbcTemplate(getDataSource());

        RowMapper<TicketStatut> vRowMapper = new RowMapper<TicketStatut>() {
            public TicketStatut mapRow(ResultSet pRS, int pRowNum) throws SQLException {
                TicketStatut vTicketStatut = new TicketStatut(pRS.getInt("id"));
                vTicketStatut.setLibelle(pRS.getString("libelle"));
                return vTicketStatut;
            }
        };

        List<TicketStatut> vListStatut = vJdbcTemplate.query(vSQL, vRowMapper);

        return vListStatut;
    }
}

Requête SQL de mise à jour

Pour les requêtes de mise à jour des données (INSERT, UPDATE...), cela se passe comme pour les requêtes de sélection sauf qu'il n'y a plus de données à charger (donc pas de RowMapper) et qu'il faut utiliser les méthodes update au lieu des méthodes query*.

public void updateTicketStatut(TicketStatut pTicketStatut) {
    String vSQL = "UPDATE statut SET libelle = :libelle WHERE id = :id";

    MapSqlParameterSource vParams = new MapSqlParameterSource();
    vParams.addValue("id", pTicketStatut.getId());
    vParams.addValue("libelle", pTicketStatut.getLibelle());

    NamedParameterJdbcTemplate vJdbcTemplate = new NamedParameterJdbcTemplate(getDataSource());

    int vNbrLigneMaJ = vJdbcTemplate.update(vSQL, vParams);
}

Passer des paramètres

Utiliser les attributs d'un JavaBean

Vous pouvez encore alléger votre code en passant directement un JavaBean en paramètre, et non plus chaque paramètre individuellement !

Pour cela, vous devez utiliser :

  • des paramètres nommés (:param) dans votre requête SQL : le nom des paramètres correspond aux attributs du JavaBean ;

  • la classe NamedParameterJdbcTemplate pour exécuter la requête

  • la classe BeanPropertySqlParameterSource comme source des paramètres en lui passant le JavaBean

Voici ce que cela donne si je reprends la requête précédente :

public void updateTicketStatut(TicketStatut pTicketStatut) {
    String vSQL = "UPDATE statut SET libelle = :libelle WHERE id = :id";

    SqlParameterSource vParams = new BeanPropertySqlParameterSource(pTicketStatut);

    NamedParameterJdbcTemplate vJdbcTemplate = new NamedParameterJdbcTemplate(getDataSource());
    int vNbrLigneMaJ = vJdbcTemplate.update(vSQL, vParams);
}
package org.example.demo.ticket.model.bean.ticket;

public class TicketStatut {
    private Integer id;
    private String libelle;

    public Integer getId() {
        return id;
    }
    public String getLibelle() {
        return libelle;
    }
    //...
}

Spécifier les types de données

Spring définit les types de données à injecter dans la requête SQL à partir des classes Java utilisées comme valeur de paramètre.

Dans certains cas, cette auto-détermination n'est pas correcte (paramètre à null par exemple) et nécessite alors de préciser le type attendu par la base de données.

Vous pouvez préciser ce type :

  • soit en l'indiquant lors de l'ajout du paramètre dans la liste des paramètres ;

  • soit en spécifiant le type des paramètres pour un BeanPropertySqlParameterSource ;

  • soit en utilisant la classe SqlParameterValue comme wrapper de votre paramètre ;

  • soit lors de l'exécution de la requête.

Je vous conseille d'utiliser l'une des 3 premières solutions, qui sont bien plus lisibles que la dernière.

package org.example.demo.ticket.consumer.impl.dao;

import java.sql.Types;
// import ...

public class TicketDaoImpl extends AbstractDaoImpl implements TicketDao {
    //...
    public void updateTicketStatut1(TicketStatut pTicketStatut) {
        String vSQL = "UPDATE statut SET libelle = :libelle WHERE id = :id";

        MapSqlParameterSource vParams = new MapSqlParameterSource();
        vParams.addValue("id", pTicketStatut.getId(), Types.INTEGER);
        vParams.addValue("libelle", pTicketStatut.getLibelle(), Types.VARCHAR);

        NamedParameterJdbcTemplate vJdbcTemplate = new NamedParameterJdbcTemplate(getDataSource());
        int vNbrLigneMaJ = vJdbcTemplate.update(vSQL, vParams);
    }


    public void updateTicketStatut2(TicketStatut pTicketStatut) {
        String vSQL = "UPDATE statut SET libelle = :libelle WHERE id = :id";

        BeanPropertySqlParameterSource vParams = new BeanPropertySqlParameterSource(pTicketStatut);
        vParams.registerSqlType("id", Types.INTEGER);
        vParams.registerSqlType("libelle", Types.VARCHAR);


        NamedParameterJdbcTemplate
            vJdbcTemplate = new NamedParameterJdbcTemplate(getDataSource());
        int vNbrLigneMaJ = vJdbcTemplate.update(vSQL, vParams);
    }


    public void updateTicketStatut3(TicketStatut pTicketStatut) {
        String vSQL = "UPDATE statut SET libelle = ? WHERE id = ?";

        Object[] vParams = {
            new SqlParameterValue(Types.INTEGER, pTicketStatut.getId()),
            new SqlParameterValue(Types.VARCHAR, pTicketStatut.getLibelle()),
        };

        JdbcTemplate vJdbcTemplate = new JdbcTemplate(getDataSource());
        vJdbcTemplate.update(vSQL, vParams);
    }


    public void updateTicketStatut4(TicketStatut pTicketStatut) {
        String vSQL = "UPDATE statut SET libelle = ? WHERE id = ?";


        Object[] vParams = {
            pTicketStatut.getId(),
            pTicketStatut.getLibelle()
        };

        JdbcTemplate vJdbcTemplate = new JdbcTemplate(getDataSource());
        vJdbcTemplate.update(vSQL,
                             vParams,
                             new int[] {
                                 Types.INTEGER,
                                 Types.VARCHAR
                             });
    }
}

Gérer les exceptions

Vous avez peut-être remarqué, mais jusqu'à présent, je ne me suis pas soucié des Exceptions pouvant survenir lors de l'exécution des requêtes SQL.

Et pour cause, Spring gère directement les SQLException et les traduits en RuntimeException. J'en ai vu qui ont bondi de leur chaise en lisant cela !

Spring fait mieux que ça, il traduit les SQLException en s'appuyant sur les codes de retour SQL pour lever des Exceptions distinctes (DataIntegrityViolationExceptionDuplicateKeyExceptionEmptyResultDataAccessException...). Toutes ces Exceptions héritent de la classe DataAccessException.

Et comme ce sont des RuntimeException, leur déclaration et leur gestion n'est pas obligatoire lors de la compilation. Ceci réduit considérablement les besoins en try...catch ! Cependant, vous pouvez toujours faire un catch de certaines de ces exceptions au cas par cas.

public void insertTicketStatut(TicketStatut pTicketStatut) {
    String vSQL = "INSERT INTO statut (id, libelle) VALUES (:id, :libelle)";
    NamedParameterJdbcTemplate vJdbcTemplate = new NamedParameterJdbcTemplate(getDataSource());

    BeanPropertySqlParameterSource vParams = new BeanPropertySqlParameterSource(pTicketStatut);
    vParams.registerSqlType("id", Types.INTEGER);
    vParams.registerSqlType("libelle", Types.VARCHAR);

    try {
        vJdbcTemplate.update(vSQL, vParams);
    } catch (DuplicateKeyException vEx) {
        LOGGER.error("Le TicketStatut existe déjà ! id=" + pTicketStatut.getId(), vEx);
        // ...
    }
}

Conclusion

Comme vous avez pu le constater, Spring JDBC simplifie grandement le travail du développeur lorsqu'il doit interagir avec une base de données relationnelle à l'aide de requêtes SQL.

Dans ce chapitre, je n'ai pas abordé toutes les fonctionnalités et les détails techniques de Spring JDBC. Je vous ai présenté ce que je considère comme la base et le principal pour l'utiliser.

Si vous souhaitez aller plus loin, n'hésitez pas à consulter la documentation officielle, notamment les parties :

Vous y trouverez également des conseils ou bonnes pratiques préconisées par les développeurs de Spring. Par exemple, sachez que la création des JdbcTemplate peut être faite à un niveau global et non par requête car cette classe est threadsafe une fois configurée (cf JdbcTemplate best practices).

Je vous conseille de vous entraîner en créant les DAO manquants du projet ticket et en y implémentant quelques méthodes.

Dans le chapitre suivant, je vous montrerai comment gérer les transactions dans un projet grâce à Spring TX.

Example of certificate of achievement
Example of certificate of achievement