Mettre en œuvre les Opérations de Base de Données (CRUD) avec Mybatis

Introduction aux Opérations CRUD avec Mybatis

Mybatis simlpifie grandement l'interaction avec les bases de données relationnelles en permettant aux développeurs de mapper des objets Java directement à des requêtes SQL. Ce guide explore comment réaliser les opérations fondamentales de Création (Create), Lecture (Read), Mise à Jour (Update) et Suppression (Delete) – communément appelées CRUD – en utilisant Mybatis.

Définition de l'Interface du Mapper

L'interface Java sert de contrat pour vos opérations de base de données. Chaque méthode déclarée dans cette interface correspondra à une opération SQL spécifique définie dans un fichier de mappage XML. Mybatis générera automatiquement l'implémentation de cete interface à l'exécution.

package com.app.data.mapper;

import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.MapKey;

/**
 * Interface de gestion des employés pour les opérations CRUD.
 */
public interface EmployeMapper {

    /**
     * Récupère un employé par son identifiant unique.
     * @param identifiant L'ID de l'employé.
     * @return L'objet Employe correspondant, ou null s'il n'est pas trouvé.
     */
    Employe obtenirEmployeParId(Integer identifiant);

    /**
     * Récupère la liste de tous les employés.
     * @return Une liste d'objets Employe.
     */
    List<Employe> obtenirTousLesEmployes();

    /**
     * Ajoute un nouvel employé à la base de données.
     * @param employe L'objet Employe à insérer.
     * @return Le nombre de lignes affectées (normalement 1 en cas de succès).
     */
    int ajouterNouvelEmploye(Employe employe);

    /**
     * Met à jour les informations d'un employé existant.
     * L'objet Employe doit contenir l'ID de l'employé à mettre à jour.
     * @param employe L'objet Employe avec les informations mises à jour.
     * @return Le nombre de lignes affectées (normalement 1 en cas de succès).
     */
    int mettreAJourEmploye(Employe employe);

    /**
     * Supprime un employé de la base de données par son identifiant.
     * @param identifiant L'ID de l'employé à supprimer.
     * @return Le nombre de lignes affectées (normalement 1 en cas de succès).
     */
    int supprimerEmploye(int identifiant);

    /**
     * Ajoute un employé en utilisant une carte de paramètres.
     * Utile pour les insertions dynamiques ou lorsque l'objet complet n'est pas nécessaire.
     * @param donneesEmploye Une Map contenant les données de l'employé (ex: "nom", "genre", "email").
     * @return Le nombre de lignes affectées.
     */
    int ajouterEmployeDepuisMap(Map<String, Object> donneesEmploye);

    /**
     * Récupère tous les employés sous forme de Map où la clé est l'ID de l'employé.
     * Nécessite l'annotation @MapKey("id") pour spécifier la clé.
     * @return Une Map où chaque clé est l'ID d'un employé et la valeur est l'objet Employe.
     */
    @MapKey("id")
    Map<Integer, Employe> obtenirTousLesEmployesEnMap();

    /**
     * Recherche des employés dont le nom contient une sous-chaîne donnée (recherche floue).
     * @param criteres Une Map contenant le critère de recherche (clé "valeur" pour la sous-chaîne).
     * @return Une liste d'objets Employe correspondant aux critères.
     */
    List<Employe> rechercherEmployesParNom(Map<String, Object> criteres);
}

Configuration du Fichier de Mappage SQL XML

Le fichier XML de mappage lie les méthodes de l'interface à des requêtes SQL spécifiques. Il est essentiel que l'attribut namespace corresponde au nom de package complet de l'interface du mapper.

<?xml version="1.0" encoding="UTF-8" ?>


<mapper namespace="com.app.data.mapper.EmployeMapper">

    <!-- Mappage des propriétés de la classe Employe avec les colonnes de la base de données -->
    <resultMap id="employeResultMap" type="com.app.data.mapper.Employe">
        <id property="id" column="id"/>
        <result property="nom" column="nom_famille"/>
        <result property="email" column="adresse_email"/>
        <result property="genre" column="sexe"/>
    </resultMap>

    <!-- Récupérer un employé par ID -->
    <select id="obtenirEmployeParId" resultMap="employeResultMap">
        SELECT id, nom_famille, adresse_email, sexe FROM tbl_employe WHERE id = #{identifiant}
    </select>

    <!-- Récupérer tous les employés -->
    <select id="obtenirTousLesEmployes" resultMap="employeResultMap">
        SELECT id, nom_famille, adresse_email, sexe FROM tbl_employe
    </select>

    <!-- Insérer un nouvel employé -->
    <insert id="ajouterNouvelEmploye" parameterType="com.app.data.mapper.Employe" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO tbl_employe (nom_famille, sexe, adresse_email) VALUES (#{nom}, #{genre}, #{email})
    </insert>

    <!-- Mettre à jour un employé existant -->
    <update id="mettreAJourEmploye" parameterType="com.app.data.mapper.Employe">
        UPDATE tbl_employe
        SET nom_famille = #{nom}, sexe = #{genre}, adresse_email = #{email}
        WHERE id = #{id}
    </update>

    <!-- Supprimer un employé par ID -->
    <delete id="supprimerEmploye" parameterType="int">
        DELETE FROM tbl_employe WHERE id = #{identifiant}
    </delete>

    <!-- Insérer un employé via une Map -->
    <insert id="ajouterEmployeDepuisMap" parameterType="map" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO tbl_employe (nom_famille, sexe, adresse_email) VALUES (#{nomEmploye}, #{genreEmploye}, #{emailEmploye})
    </insert>

    <!-- Récupérer tous les employés dans une Map (clé: ID) -->
    <select id="obtenirTousLesEmployesEnMap" resultMap="employeResultMap">
        SELECT id, nom_famille, adresse_email, sexe FROM tbl_employe
    </select>

    <!-- Recherche floue d'employés par nom -->
    <select id="rechercherEmployesParNom" parameterType="map" resultMap="employeResultMap">
        SELECT id, nom_famille, adresse_email, sexe
        FROM tbl_employe
        WHERE nom_famille LIKE CONCAT('%', #{motCle}, '%')
    </select>
</mapper>

Notez l'utilisation de resultMap pour un mappage explicite des colonnes de la base de données (nom_famille, adresse_email, sexe) aux propriétés de l'objet Java (nom, email, genre). Pour l'insertion, useGeneratedKeys="true" keyProperty="id" est utilisé pour récupérer l'ID généré par la base de données.

Tests des Opérations CRUD

Pour valider nos implémentations, nous utiliserons un bloc de test qui initialise la session Mybatis et exécute chaque opération CRUD. Nous allons d'abord créer une classe Employe simple avec les propriétés nécessaires.

package com.app.data.mapper;

/**
 * Représente un employé.
 */
public class Employe {
    private Integer id;
    private String nom; // Correspond à nom_famille dans la DB
    private String email; // Correspond à adresse_email
    private String genre; // Correspond à sexe

    // Constructeur par défaut
    public Employe() {}

    // Constructeur sans ID (pour l'insertion)
    public Employe(String nom, String genre, String email) {
        this.nom = nom;
        this.genre = genre;
        this.email = email;
    }

    // Constructeur avec ID (pour la mise à jour, lecture)
    public Employe(Integer id, String nom, String genre, String email) {
        this.id = id;
        this.nom = nom;
        this.genre = genre;
        this.email = email;
    }

    // Getters
    public Integer getId() { return id; }
    public String getNom() { return nom; }
    public String getEmail() { return email; }
    public String getGenre() { return genre; }

    // Setters
    public void setId(Integer id) { this.id = id; }
    public void setNom(String nom) { this.nom = nom; }
    public void setEmail(String email) { this.email = email; }
    public void setGenre(String genre) { this.genre = genre; }

    @Override
    public String toString() {
        return "Employe{" +
               "id=" + id +
               ", nom='" + nom + '\'' +
               ", email='" + email + '\'' +
               ", genre='" + genre + '\'' +
               '}';
    }
}

Voici un exemple de code de test utilisant JUnit pour valider les opérations CRUD.

import com.app.data.mapper.Employe;
import com.app.data.mapper.EmployeMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

public class EmployeServiceTest {

    private static SqlSessionFactory fabriqueSessionSql;
    private SqlSession sessionSql;
    private EmployeMapper mappeurEmploye;

    // Méthode exécutée une seule fois avant tous les tests
    @BeforeAll
    static void setupGlobal() throws IOException {
        String ressource = "mybatis-config.xml"; // Assurez-vous que ce fichier existe
        InputStream flux = Resources.getResourceAsStream(ressource);
        fabriqueSessionSql = new SqlSessionFactoryBuilder().build(flux);
    }

    // Méthode exécutée avant chaque test
    @BeforeEach
    void setup() {
        sessionSql = fabriqueSessionSql.openSession(); // Ouvre une nouvelle session pour chaque test
        mappeurEmploye = sessionSql.getMapper(EmployeMapper.class);
    }

    // Méthode exécutée après chaque test
    @AfterEach
    void tearDown() {
        if (sessionSql != null) {
            sessionSql.close(); // Ferme la session après chaque test
        }
    }

    @Test
    void testOperationsCRUDStandard() {
        System.out.println("--- Démarrage des Tests CRUD Standard ---");

        // 1. Insertion d'un nouvel employé
        Employe nouvelEmploye = new Employe("Dupont", "Homme", "dupont@exemple.com");
        int lignesAjoutees = mappeurEmploye.ajouterNouvelEmploye(nouvelEmploye);
        sessionSql.commit(); // Important pour sauvegarder les modifications
        assertTrue(lignesAjoutees > 0, "L'insertion de l'employé devrait réussir.");
        assertNotNull(nouvelEmploye.getId(), "L'ID de l'employé devrait être généré.");
        System.out.println("Employé ajouté : " + nouvelEmploye);
        Integer idNouvelEmploye = nouvelEmploye.getId();

        // 2. Lecture d'un employé par ID
        Employe employeLu = mappeurEmploye.obtenirEmployeParId(idNouvelEmploye);
        assertNotNull(employeLu, "L'employé devrait être trouvé par son ID.");
        assertEquals("Dupont", employeLu.getNom(), "Le nom de l'employé lu devrait correspondre.");
        System.out.println("Employé lu : " + employeLu);

        // 3. Mise à jour d'un employé
        employeLu.setEmail("nouveau.dupont@exemple.com");
        employeLu.setGenre("Inconnu");
        int lignesMisesAJour = mappeurEmploye.mettreAJourEmploye(employeLu);
        sessionSql.commit();
        assertTrue(lignesMisesAJour > 0, "La mise à jour de l'employé devrait réussir.");
        Employe employeMisAJour = mappeurEmploye.obtenirEmployeParId(idNouvelEmploye);
        assertEquals("nouveau.dupont@exemple.com", employeMisAJour.getEmail(), "L'email de l'employé devrait être mis à jour.");
        System.out.println("Employé mis à jour : " + employeMisAJour);

        // 4. Lecture de tous les employés
        List<Employe> tousLesEmployes = mappeurEmploye.obtenirTousLesEmployes();
        assertFalse(tousLesEmployes.isEmpty(), "La liste des employés ne devrait pas être vide.");
        System.out.println("Liste de tous les employés (" + tousLesEmployes.size() + ") : ");
        tousLesEmployes.forEach(System.out::println);

        // 5. Suppression d'un employé
        int lignesSupprimees = mappeurEmploye.supprimerEmploye(idNouvelEmploye);
        sessionSql.commit();
        assertTrue(lignesSupprimees > 0, "La suppression de l'employé devrait réussir.");
        Employe employeSupprime = mappeurEmploye.obtenirEmployeParId(idNouvelEmploye);
        assertNull(employeSupprime, "L'employé ne devrait plus exister après suppression.");
        System.out.println("Employé avec ID " + idNouvelEmploye + " supprimé.");

        System.out.println("--- Fin des Tests CRUD Standard ---");
    }

    @Test
    void testInsertionAvecMap() {
        System.out.println("\n--- Démarrage du Test d'Insertion avec Map ---");
        Map<String, Object> donnees = new HashMap<>();
        donnees.put("nomEmploye", "Martin");
        donnees.put("genreEmploye", "Femme");
        donnees.put("emailEmploye", "martin@societe.com");

        int result = mappeurEmploye.ajouterEmployeDepuisMap(donnees);
        sessionSql.commit();
        assertTrue(result > 0, "L'insertion via Map devrait réussir.");
        System.out.println("Employé inséré via Map. ID généré: " + donnees.get("id"));
        assertNotNull(donnees.get("id"), "L'ID devrait être disponible après insertion via Map.");
        System.out.println("--- Fin du Test d'Insertion avec Map ---");
    }

    @Test
    void testRecuperationTousEmployesEnMap() {
        System.out.println("\n--- Démarrage du Test de Récupération de Map d'Employés ---");
        // Assurons-nous qu'il y a au moins un employé
        mappeurEmploye.ajouterNouvelEmploye(new Employe("Durand", "Homme", "durand@exemple.com"));
        sessionSql.commit();

        Map<Integer, Employe> employesParId = mappeurEmploye.obtenirTousLesEmployesEnMap();
        assertFalse(employesParId.isEmpty(), "La map d'employés ne devrait pas être vide.");
        System.out.println("Employés récupérés en Map :");
        employesParId.forEach((id, emp) -> System.out.println("ID: " + id + ", Employé: " + emp));
        System.out.println("--- Fin du Test de Récupération de Map d'Employés ---");
    }

    @Test
    void testRechercheFloue() {
        System.out.println("\n--- Démarrage du Test de Recherche Floue ---");
        // Ajouter des données de test
        mappeurEmploye.ajouterNouvelEmploye(new Employe("Lemaire", "Homme", "lem@exemple.com"));
        mappeurEmploye.ajouterNouvelEmploye(new Employe("Leroy", "Femme", "ler@exemple.com"));
        mappeurEmploye.ajouterNouvelEmploye(new Employe("Petit", "Homme", "pet@exemple.com"));
        sessionSql.commit();

        Map<String, Object> criteresRecherche = new HashMap<>();
        criteresRecherche.put("motCle", "Le"); // Rechercher "Le" dans le nom

        List<Employe> employesTrouves = mappeurEmploye.rechercherEmployesParNom(criteresRecherche);
        assertFalse(employesTrouves.isEmpty(), "La recherche floue devrait trouver des résultats.");
        System.out.println("Employés trouvés avec 'Le' : ");
        employesTrouves.forEach(System.out::println);
        assertTrue(employesTrouves.stream().anyMatch(e -> e.getNom().contains("Lemaire")), "Devrait trouver 'Lemaire'");
        System.out.println("--- Fin du Test de Recherche Floue ---");
    }
}

Pour exécuter ces tests, vous aurez besoin d'un fichier mybatis-config.xml dans votre classpath, configuré pour se connecter à une base de données avec une table tbl_employe. Voici un exemple minimal de cette configuration :

<?xml version="1.0" encoding="UTF-8" ?>

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/votre_base_de_donnees?serverTimezone=UTC"/>
                <property name="username" value="votre_utilisateur"/>
                <property name="password" value="votre_mot_de_passe"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/app/data/mapper/EmployeMapper.xml"/>
    </mappers>
</configuration>

Et la structure de la table SQL tbl_employe pourrait ressembler à ceci :

CREATE TABLE tbl_employe (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nom_famille VARCHAR(100) NOT NULL,
    adresse_email VARCHAR(100) NOT NULL,
    sexe VARCHAR(10)
);

Étiquettes: MyBatis crud SQL Java Mapper

Publié le 2 septembre à 00h49