Conception d'un Framework ORM Léger Inspiré de MyBatis en Java

Vue d'ensemble technique

Le développement d'un outil de mappage objet-relationnel (ORM) personnalisé offre une compréhension approfondie des mécanismes d'interaction avec les bases de données. Cet article détaille l'architecture et l'implémentation d'un ORM Java léger, conçu de zéro sans dépendances tierces lourdes, à l'exception des utilitaires de réflexion de base pour la copie de propriétés. L'objectif est de reproduire les fonctionnalités essentielles d'un framework de persistance de niveau entreprise, notamment l'analyse syntaxique SQL, la liaison de paramètres, l'hydratation des résultats et la gestion des pools de connexions.

Mécanismes fondamentaux

1. Mappage par Annotations

L'approche par annotation permet de définir les requêtes SQL directement sur les méthodes d'interface. Le framework prend en charge les opérations CRUD via des annotations personnalisées telles que @Select, @Insert, @Update et @Delete. La liaison de paramètres dynamiques est gérée par la syntaxe #{param}, avec une prise en charge native des types primitifs, des objets POJO, des collections Map et des paramètres multiples via l'annotation @Param.

2. Analyse SQL et Liaison de Paramètres

Le moteur d'analyse syntaxique intercepte les chaînes SQL pour identifier les espaces réservés #{}. Il les remplace par des marqueurs de position JDBC (?) et extrait les valeurs correspondantes via réflexion. Une fonctionnalité intégrée permet la conversion automatique des noms de propriétés Java en camelCase vers le format snake_case de la base de données, assurant ainsi une correspondance transparente des colonnes.

3. Hydratation des Jeux de Résultats

Le composant de mappage des résultats transforme les objets ResultSet JDBC en instances Java. Il gère nativement les retours uniques, les collections (List), les structures clé-valeur (Map) et les types scalaires. Il inclut également un mécanisme pour récupérer et injecter les clés primaires auto-générées directement dans l'objet persisté après une insertion.

4. Pool de Connexions et Transactions

Un gestionnaire de connexions interne est implémenté pour éviter la surcharge liée à la création et à la fermeture fréquentes des sockets de base de données. Il prend en charge l'initialisation d'un pool fixe et l'expansion dynamique. La couche transactionnelle expose des méthodes explicites pour le commit et le rollback, permettant de basculer entre un mode auto-commit et un contrôle transactionnel manuel.

5. Stratégie de Mise en Cache

Le framework intègre une architecture de cache à deux niveaux. Le cache de premier niveau (L1) est lié au cycle de vie de la session et utilise une structure concurrente pour stocker les résultats basés sur le hachage de la requête et des paramètres. Le cache de second niveau (L2) est partagé globalement et peut être invalidé de manière granulaire par nom de table pour prévenir les lectures sales.

Architecture du Système

La conception suit un modèle en couches strict pour séparer les responsabilités :

  • Couche de Session : Gère le cycle de vie des sessions et la création des proxys dynamiques pour les interfaces DAO.
  • Couche d'Analyse : Décompose les requêtes SQL, prépare les instructions JDBC et mappe les résultats.
  • Couche d'Infrastructure : Gère la configuration, le pool de connexions et le cache global.

Implémentation et Exemples de Code

Configuration de l'Environnement

Les paramètres de connexion et les options du framework sont externalisés dans un fichier de propriétés :

# Paramètres de la base de données
db.url=jdbc:mysql://localhost:3306/inventory_db?useSSL=false&characterEncoding=UTF-8
db.driver=com.mysql.cj.jdbc.Driver
db.user=admin
db.pass=secure_password

# Configuration du pool
pool.initialSize=10
pool.incrementSize=5

# Options ORM
orm.autoCommit=false
orm.mapCamelCase=true
orm.enableL2Cache=true

Définition de l'Interface de Persistance

Les opérations de base de données sont déclarées via une interface utilisant les annotations du framework :

public interface ProductDao {
    
    @Insert("INSERT INTO products (name, price, quantity) VALUES (#{name}, #{price}, #{quantity})")
    int save(Product product);
    
    @Insert(value = "INSERT INTO products (name, price, quantity) VALUES (#{name}, #{price}, #{quantity})", returnGeneratedKeys = true)
    int saveAndReturnId(Product product);
    
    @Select("SELECT * FROM products WHERE id = #{id}")
    Product findById(@Param("id") Long id);
    
    @Select("SELECT * FROM products WHERE quantity > #{minQty}")
    List<Product> findAvailableProducts(@Param("minQty") int minQty);
    
    @Update("UPDATE products SET price = #{price} WHERE id = #{id}")
    int updatePrice(Product product);
    
    @Delete("DELETE FROM products WHERE id IN (${ids})")
    int deleteBatch(@Param("ids") List<Long> ids);
}

Exécution et Gestion des Sessions

L'initialisation du framework et l'exécution des requêtes s'effectuent via le gestionnnaire de sessions :

public class ApplicationRunner {
    public static void main(String[] args) {
        SessionFactory factory = new SessionFactory("db.properties");
        
        try (SqlSession session = factory.openSession()) {
            ProductDao dao = session.getMapper(ProductDao.class);
            
            Product newItem = new Product("Mechanical Keyboard", 149.99, 200);
            dao.saveAndReturnId(newItem);
            session.commit();
            
            Product fetchedItem = dao.findById(newItem.getId());
            System.out.println("Produit récupéré : " + fetchedItem.getName());
            
        } catch (PersistenceException e) {
            System.err.println("Échec de l'opération de base de données : " + e.getMessage());
        }
    }
}

Contrôle Transactionnel Menuel

Pour les opérations nécessitant une atomicité stricte, la session peut être configurée pour désactiver l'auto-commit, permettant un contrôle explicite des limites de transaction :

SessionFactory factory = new SessionFactory("db.properties");
SqlSession session = factory.openSession(false); // Désactive l'auto-commit

try {
    ProductDao dao = session.getMapper(ProductDao.class);
    
    Product item1 = new Product("Monitor", 299.00, 50);
    Product item2 = new Product("Mouse", 45.00, 300);
    
    dao.save(item1);
    dao.save(item2);
    
    session.commit(); // Valide les deux insertions de manière atomique
} catch (Exception e) {
    session.rollback(); // Annule toutes les opérations en cas d'erreur
    throw new RuntimeException("Transaction échouée", e);
} finally {
    session.close(); // Libère la connexion vers le pool
}

Étiquettes: Java ORM MyBatis JDBC Reflection

Publié le 31 juillet à 15h41