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
- Eficiência de Recursos: Ao utilizar variáveis de ligação, o Oracle reutiliza planos de execução, reduzindo drasticamente o consumo de CPU.
- Segurança: Evita problemas de SQL Injection que poderiam surgir via concatenação de strings.
- 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.
- 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.