Otimizando Paginação Física no Oracle com MyBatis: Como Evitar o Problema de Hard Parse

O RowBounds padrão do MyBatis realiza o que chamamos de paginação em memória. Ele recupera todo o conjunto de dados do banco de dados e, somente no lado da aplicação, extrai o subconjunto desjeado. Em sistemas com grandes volumes de dados, essa abordagem é um caminho rápido para erros de OutOfMemoryError (OOM).

A solução convencional no ecossistema Java é implementar um Interceptor que atue no StatementHandler.prepare, modificando o SQL original para incluir as cláusulas de paginação específicas de cada banco de dados antes da execução.

O Perigo da Interpolação de Strings no SQL

Muitos desenvolvedores cometem o erro de concatenar os valores de offset e limit diretamente na String do SQL, conforme o exemplo abaixo:

String sqlPaginado = sqlOriginal + " LIMIT " + deslocamento + "," + limite;

Para o banco de dados Oracle, cada variação desses valores resulta em uma String SQL diferente. Isso força o banco a realizar um Hard Parse (análise pesada) para cada consulta. O Hard Parse consome CPU de forma intensiva e sobrecarrega a shared pool. Em ambientes de alta concorrência, isso degrada a performance global do sistema, evidenciando problemas como library cache lock nos relatórios AWR.

A solução correta é utilizar Bind Variables (o símbolo ?), garentindo que o template do SQL seja fixo e o banco realize apenas o Soft Parse.

-- Template único, analisado apenas uma vez pelo Oracle
SELECT * FROM (SELECT a.*, ROWNUM rnum FROM (SELECT * FROM usuarios) a WHERE ROWNUM <= ?) WHERE rnum > ?

Superando a Imutabilidade no MyBatis

Um obstáculo técnico no MyBatis é que o método BoundSql.getParameterMappings() retorna uma lista protegida por Collections.unmodifiableList(). Tentar modificar essa lista diretamente resulta em UnsupportedOperationException.

A estratégia aplicada aqui consiste em uma pequena alteração no código-fonte do MyBatis para converter essa lista em um ArrayList comum, permitindo que o interceptador manipule os parâmetros dinamicamente.

Implementação do Interceptador de Paginação

O objetivo principal é remover mapeamentos automáticos e reinseri-los na ordem exata exigida pelos placeholders ? no SQL de paginação.

package com.projeto.core.mybatis;

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class InterceptadorPaginacao implements Interceptor {

    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();
        MetaObject metaObject = MetaObject.forObject(handler, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(), new DefaultReflectorFactory());
        
        RowBounds rowBounds = (RowBounds) metaObject.getValue("delegate.rowBounds");

        if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {
            return invocation.proceed();
        }

        // Recupera o objeto de parâmetros (deve estender uma classe base que suporte campos de paginação)
        BaseDTO parametro = (BaseDTO) boundSql.getParameterObject();
        parametro.setLinhaFinal(rowBounds.getOffset() + rowBounds.getLimit());
        parametro.setLinhaInicial(rowBounds.getOffset());

        List<ParameterMapping> mapeamentos = boundSql.getParameterMappings();
        Configuration config = (Configuration) metaObject.getValue("delegate.configuration");

        // Remove mapeamentos existentes para evitar conflitos de ordem
        mapeamentos.removeIf(m -> "linhaFinal".equals(m.getProperty()) || "linhaInicial".equals(m.getProperty()));

        // Adiciona os parâmetros na ordem correta para o SQL paginado
        TypeHandler<Integer> intHandler = new IntegerTypeHandler();
        
        mapeamentos.add(new ParameterMapping.Builder(config, "linhaFinal", intHandler).build());
        mapeamentos.add(new ParameterMapping.Builder(config, "linhaInicial", intHandler).build());

        String sqlOriginal = (String) metaObject.getValue("delegate.boundSql.sql");
        String dialetoTipo = config.getVariables().getProperty("dialect");
        
        Dialeto bancoDialeto = FabricaDialeto.obter(dialetoTipo);
        metaObject.setValue("delegate.boundSql.sql", bancoDialeto.gerarSqlPaginado(sqlOriginal));

        // Reseta o RowBounds para evitar a paginação em memória do MyBatis
        metaObject.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
        metaObject.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);

        return invocation.proceed();
    }

    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    public void setProperties(Properties properties) {}
}

Dialetos de Banco de Dados

A abstração permite suportar múltiplos bancos. Abaixo, a lógica para o Oracle, que lida com subconsultas e a cláusula FOR UPDATE.

package com.projeto.core.mybatis;

public class OracleDialeto extends Dialeto {

    public String gerarSqlPaginado(String sql) {
        sql = sql.trim();
        boolean locked = false;
        if (sql.toLowerCase().endsWith(" for update")) {
            sql = sql.substring(0, sql.length() - 11);
            locked = true;
        }

        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
        sqlBuilder.append("SELECT * FROM ( SELECT inner_.*, ROWNUM rnum_ FROM ( ");
        
        String tempSql = sql.toLowerCase();
        if (tempSql.contains("where")) {
            // Lógica simplificada: injeta ROWNUM no filtro se não houver ordenação complexa
            if (!tempSql.contains("order by") && !tempSql.contains("group by")) {
                sql += " AND ROWNUM <= ? ";
            } else {
                sqlBuilder.append(sql);
                sqlBuilder.append(" ) inner_ WHERE ROWNUM <= ? ");
            }
        } else {
            sql += " WHERE ROWNUM <= ? ";
        }

        if (!sqlBuilder.toString().contains("inner_")) {
            sqlBuilder.append(sql).append(" ) inner_ ");
        }

        sqlBuilder.append(") WHERE rnum_ > ?");

        if (locked) {
            sqlBuilder.append(" FOR UPDATE");
        }
        
        return sqlBuilder.toString();
    }
}

Vantagens Desta Abordagem

  1. Eficiência de Recursos: Ao utilizar variáveis de ligação, o Oracle reutiliza planos de execução, reduzindo drasticamente o consumo de CPU.
  2. Segurança: Evita problemas de SQL Injection que poderiam surgir via concatenação de strings.
  3. Performance: A eliminação da paginação em memória previne gargalos de heap e latência de rede desnecessária, trazendo apenas as linhas solicitadas.
  4. Flexibilidade: A estrutura de dialetos permite que a mesma aplicação suporte MySQL (cláusula LIMIT) ou SQL Server (OFFSET/FETCH ou ROW_NUMBER()) com alterações mínimas.

Tags: MyBatis Oracle SQL java performance

Publicado em 6-15 03:31 por Thomas