Les Exécuteurs MyBatis : Interface et Implémentations

MyBatis propose une architecture d'exécution flexible pour enteragir avec la base de données. Le composant clé responsable de cette interaction est l'interface Executor.

L'Interface Executor

L'interface Executor définit les opérations fondamentales pour l'exécution des requêtes et des mises à jour. Elle inclut des méthodes pour la mise à jour des données, l'exécution de requêtes (avec ou sans gestion de résultats et pagination), la gestion des transactions (commit, rollback), la manipulation du cache local, et la création de clés de cache.

Méthodes Clés de Executor :

  • update(MappedStatement ms, Object parameter): Exécute une opération de mise à jour (INSERT, UPDATE, DELETE).
  • query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql): Exécute une requête, en tenant compte du cache, des limites de lignes et d'un gestionnaire de résultats.
  • commit(boolean required): Valide la transaction actuelle.
  • rollback(boolean required): Annule la transaction actuelle.
  • clearLocalCache(): Vide le cache de premier niveau associé à l'exécuteur.
  • createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql): Génère une clé unique pour le cache basée sur les détails de la requête.

BaseExecutor : La Classe Abstraite

BaseExecutor est une classe abstraite qui implémente l'interface Executor et fournit une implémentation par défaut pour de nombreuses fonctionnalités. Elle utilise le pattern Template Method, laissant les méthodes spécifiques à l'exécution (comme doUpdate et doQuery) à ses sous-classes.

Fonctionnalités de BaseExecutor :

  • Cache de Premier Niveau : Gère le cache local (par requête) à l'aide de PerpetualCache. Ce cache est vidé lors d'une opération de mise à jour.
  • Gestion des Transactions : Travaille avec l'objet Transaction fourni.
  • Chargement Différé : Supporte le chargement différé des associations et des collections.

Implémentations des Méthodes dans BaseExecutor :

  • La méthode update vide le cache local avant d'appeler la méthode abstraite doUpdate.
  • La méthode query implémente la logique de recherche dans le cache local avant de déléguer l'appel à queryFromDatabase (qui appelle doQuery) en cas de cache non trouvé.
  • La méthode createCacheKey génère la clé de cache en combinant l'ID de la requête, les paramètres, le SQL, les informations de pagination et les détails de l'environnement.

Méthodes Abstraites :

Ces méthodes doivent être implémentées par les sous-classes pour gérer l'exécution réelle des opérations SQL :

  • protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
  • protected abstract List<batchresult> doFlushStatements(boolean isRollback) throws SQLException;</batchresult>
  • protected abstract <e> List<e> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;</e></e>
  • protected abstract <e> Cursor<e> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException;</e></e>

SimpleExecutor

SimpleExecutor est l'implémentation la plus basique. Pour chaque requête, il crée un nouveau Statement JDBC, l'exécute via le StatementHandler, puis ferme le Statement. Il est configuré par défaut dans mybatis-config.xml.


public class SimpleExecutor extends BaseExecutor {

    public SimpleExecutor(Configuration configuration, Transaction transaction) {
        super(configuration, transaction);
    }

    @Override
    public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
        Statement stmt = null;
        try {
            StatementHandler handler = ms.getConfiguration().newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
            stmt = prepareStatement(handler, ms.getStatementLog());
            return handler.update(stmt);
        } finally {
            closeStatement(stmt);
        }
    }

    @Override
    public <e> List<e> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
            StatementHandler handler = ms.getConfiguration().newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            stmt = prepareStatement(handler, ms.getStatementLog());
            return handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }

    // ... autres méthodes ...
}
</e></e>

BatchExecutor

BatchExecutor optimise les performances en regroupant plusieurs opérations de mise à jour en un seul appel JDBC (batch). Il maintient une liste de Statement et de BatchResult. Il faut explicitement appeler flushStatements() pour exécuter les opérations en batch accumulées.


public class BatchExecutor extends BaseExecutor {

    private final List<Statement> statementList = new ArrayList<>();
    private final List<BatchResult> batchResultList = new ArrayList<>();
    private String currentSql;
    private MappedStatement currentStatement;

    public BatchExecutor(Configuration configuration, Transaction transaction) {
        super(configuration, transaction);
    }

    @Override
    public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
        // Logique pour ajouter l'opération à un batch existant ou en créer un nouveau
        // ...
        handler.batch(stmt);
        return BATCH_UPDATE_RETURN_VALUE; // Valeur spéciale pour indiquer une opération batch
    }

    // ... autres méthodes, y compris flushStatements() qui exécute les batchs ...
}

ReuseExecutor

ReuseExecutor améliore les performances en réutilisant les objets Statement JDBC pour des requêtes SQL identiques au sein d'une même session. Il utilise une HashMap pour stocker et récupérer les Statement existants.


public class ReuseExecutor extends BaseExecutor {

    private final Map<String, Statement> statementMap = new HashMap<>();

    public ReuseExecutor(Configuration configuration, Transaction transaction) {
        super(configuration, transaction);
    }

    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        BoundSql boundSql = handler.getBoundSql();
        String sql = boundSql.getSql();
        Statement stmt;
        if (hasStatementFor(sql)) {
            stmt = getStatement(sql); // Réutilisation du Statement
            applyTransactionTimeout(stmt);
        } else {
            Connection connection = getConnection(statementLog);
            stmt = handler.prepare(connection, transaction.getTimeout());
            putStatement(sql, stmt); // Mise en cache du nouveau Statement
        }
        handler.parameterize(stmt);
        return stmt;
    }
    
    // ... autres méthodes ...
}

CachingExecutor

CachingExecutor est responsable de l'activation du cache de second niveau de MyBatis. Il agit comme un proxy (Proxy Pattern) pour un autre Executor (par exemple, SimpleExecutor). Avant d'exécuter une requête, il vérifie le cache de second niveau. S'il y a un hit, il retourne le résultat du cache ; sinon, il délègue l'exécution à l'Executor sous-jacent et met le résultat en cache.


public class CachingExecutor implements Executor {
    private final Executor delegate; // L'exécuteur délégué
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

    public CachingExecutor(Executor delegate) {
        this.delegate = delegate;
        delegate.setExecutorWrapper(this); // Permet la détection des wrappers
    }

    @Override
    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
        flushCacheIfRequired(ms); // Gère le vidage du cache si nécessaire
        return this.delegate.update(ms, parameterObject);
    }

    @Override
    public <e> List<e> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        Cache cache = ms.getCache();
        if (cache != null && ms.isUseCache() && resultHandler == null) {
            // Tente de récupérer le résultat du cache de second niveau
            List<e> list = (List<E>) this.tcm.getObject(cache, key);
            if (list == null) {
                // Cache non trouvé, délègue à l'exécuteur sous-jacent
                list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // Met le résultat en cache
                this.tcm.putObject(cache, key, list);
            }
            return list;
        }
        // Si pas de cache ou pas de hit, délègue directement
        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    private void flushCacheIfRequired(MappedStatement ms) {
        Cache cache = ms.getCache();
        if (cache != null && ms.isFlushCacheRequired()) {
            this.tcm.clear(cache); // Vide le cache
        }
    }
    
    // ... autres méthodes déléguées ...
}
</e></e></e>

Étiquettes: MyBatis executor baseexecutor simpleexecutor batchexecutor

Publié le 29 juin à 18h09