Implementación de un Plugin de Paginación Personalizado para MyBatis

Configuración del Entorno

Primero, creamos un proyecto Maven e incorporamos las dpeendencias necesarias de MyBatis y MySQL.

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.0.4</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.15</version>
</dependency>

En el archivo pom, también debemos incluir el siguiente plugin para evitar posibles errores:

  <build>
    <resources>
      <!-- Recursos del proyecto -->
      <resource>
        <directory>src/main/java</directory>
        <!-- Incluir archivos de mapeo -->
        <includes>
          <include>**/*.xml</include>
        </includes>
      </resource>
    </resources>
  </build>

Archivo de Configuración de Base de Datos

En el directorio 【resources】, creamos el archivo 【db.properties】.

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC&useSSL=false
username=root
password=root

Archivo de Configuración de MyBatis

En el directorio 【resources】, creamos el archivo 【mybatis-config.xml】.

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

<!-- Archivo de configuración XML con ajustes principales del sistema MyBatis -->
<configuration>
    <!-- Especificar archivo de configuración de base de datos -->
    <properties resource="db.properties" />
    <!-- Configurar plugin de paginación personalizado -->
    <plugins>
        <!-- Plugin de paginación personalizado -->
        <plugin interceptor="com.gitee.zhuyb.interceptor.PageInterceptor">
            <property name="dialecto" value="mysql"/>
        </plugin>
        <!-- Plugin para procesamiento de conjuntos de resultados -->
        <plugin interceptor="com.gitee.zhuyb.interceptor.ResultInterceptor">
        </plugin>
    </plugins>
    <!-- Configuración del entorno de base de datos -->
    <environments default="mysql">
        <!-- Configuración del entorno, es decir, la base de datos conectada -->
        <environment id="mysql">
            <!-- Tipo de gestión de transacciones, type="JDBC" indica un uso directo simple de la configuración de提交 y 回滚 de JDBC -->
            <transactionManager type="JDBC" />
            <!-- Configuración del dataSource, POOLED es la implementación de pool de conexiones del dataSource de objetos de conexión JDBC -->
            <dataSource type="POOLED">
                <property name="driver" value="${driver}" />
                <property name="url" value="${url}" />
                <property name="username" value="${username}" />
                <property name="password" value="${password}" />
            </dataSource>
        </environment>
    </environments>
    <!-- Configurar ruta de archivos de mapeo -->
    <mappers>
        <!-- Configurar ruta del archivo de mapeo UserMapper.xml -->
        <mapper resource="com/gitee/zhuyb/mapper/SysUserMapper.xml" />
    </mappers>
</configuration>

Creación del Objeto de Paginación PaginacionVO

package com.gitee.zhuyb.dominio;
 
import java.util.List;
 
public class PaginacionVO {
    private Integer indicePagina;
    private Integer tamanioPagina;
    private Integer totalPaginas;
    private Integer totalRegistros;
    private List datos;
 
    public PaginacionVO(Integer indicePagina, Integer tamanioPagina) {
        this.indicePagina = indicePagina;
        this.tamanioPagina = tamanioPagina;
    }
 
    public Integer getIndicePagina() {
        return indicePagina;
    }
 
    public void setIndicePagina(Integer indicePagina) {
        this.indicePagina = indicePagina;
    }
 
    public Integer getTamanioPagina() {
        return tamanioPagina;
    }
 
    public void setTamanioPagina(Integer tamanioPagina) {
        this.tamanioPagina = tamanioPagina;
    }
 
    public Integer getTotalPaginas() {
        return totalPaginas;
    }
 
    public void setTotalPaginas(Integer totalPaginas) {
        this.totalPaginas = totalPaginas;
    }
 
    public Integer getTotalRegistros() {
        return totalRegistros;
    }
 
    public void setTotalRegistros(Integer totalRegistros) {
        this.totalRegistros = totalRegistros;
    }
 
    public List getDatos() {
        return datos;
    }
 
    public void setDatos(List datos) {
        this.datos = datos;
    }
 
    @Override
    public String toString() {
        return "PaginacionVO{" +
                "\n  indicePagina=" + indicePagina +
                "\n, tamanioPagina=" + tamanioPagina +
                "\n, totalPaginas=" + totalPaginas +
                "\n, totalRegistros=" + totalRegistros +
                "\n, datos=" + datos +
                "\n}";
    }
}

Creación de la Clase Utilitaria PaginacionUtil

La clase PaginacionUtil utiliza una variable de hilo local para guardar el objeto PaginacionVO.

package com.gitee.zhuyb.dominio;
 
public class PaginacionUtil {
    private static final ThreadLocal<PaginacionVO> LOCAL_PAGINA = new ThreadLocal<PaginacionVO>();
 
    // Iniciar objeto de paginación, establecer PaginacionVO
    public static PaginacionVO iniciar(PaginacionVO paginacionVo) {
        LOCAL_PAGINA.set(paginacionVo);
        return paginacionVo;
    }
 
    // Finalizar objeto de paginación, puede obtener PaginacionVO con conjunto de resultados
    public static PaginacionVO finalizar() {
        PaginacionVO paginacionVo = PaginacionUtil.obtenerPaginacionVO();
        LOCAL_PAGINA.remove();
        return paginacionVo;
    }
 
    // Obtener objeto de parámetros de paginación
    public static PaginacionVO obtenerPaginacionVO() {
        return LOCAL_PAGINA.get();
    }
}

Creación del Interceptor de Parámetros de Paginación PageInterceptor

El principio del plugin de paginación personalizado de MyBatis: interceptar la sentencia SQL de ejecución a través del interceptor, luego concatenar la sentencia de paginación, y finalmente ejecutar la sentencia SQL completa concatenada.

package com.gitee.zhuyb.interceptor;
 
import com.gitee.zhuyb.dominio.PaginacionUtil;
import com.gitee.zhuyb.dominio.PaginacionVO;
import org.apache.ibatis.executor.parameter.DefaultParameterHandler;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.PreparedStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

@Intercepts({@Signature(type=StatementHandler.class, method="prepare", args=Connection.class)})
public class PageInterceptor implements Interceptor {
 
    /** Tipo de base de datos */
    private String dialecto;
 
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
        MetaObject delegateMetaObject =MetaObject.forObject(statementHandler);
        PreparedStatementHandler preparedStatementHandler = (PreparedStatementHandler)delegateMetaObject.getValue("delegado");
 
        BoundSql boundSql = preparedStatementHandler.getBoundSql();
        Object parameterObject = boundSql.getParameterObject();
        Connection connection = (Connection)invocation.getArgs()[0];
 
        // Obtener parámetros de paginación
        PaginacionVO paginacionVo = PaginacionUtil.obtenerPaginacionVO();
        // Si se ha activado la paginación
        if(paginacionVo != null) {
            // Concatenar parámetros de paginación
            String sqlPaginado = this.obtenerSqlPaginado(boundSql.getSql(), paginacionVo);
            // Calcular número total de registros
            this.contarTotal(paginacionVo, parameterObject, preparedStatementHandler, connection);
            // Establecer nuevo sql
            MetaObject boundSqlMetaObject = MetaObject.forObject(boundSql);
            boundSqlMetaObject.setValue("sql", sqlPaginado);
        }
        // Ejecutar operaciones posteriores
        return invocation.proceed();
    }
 
    @Override
    public Object plugin(Object target) {
        // Establecer objeto proxy
        return Plugin.wrap(target,this);
    }
 
    @Override
    public void setProperties(Properties properties) {
        // Establecer propiedades
        this.dialecto = properties.getProperty("dialecto");
    }
 
 
    /********************************************************************/
 
    /**
     * Calcular número total de registros y páginas totales
     * @param paginacionVo
     * @param parameterObject
     * @param statementHandler
     * @param connection
     */
    private void contarTotal(PaginacionVO paginacionVo, Object parameterObject, PreparedStatementHandler statementHandler, Connection connection){
        MetaObject metaObject = MetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("mappedStatement");
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        // Obtener SQL de conteo
        sql = this.obtenerSqlConteo(sql);
        BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings(), parameterObject);
 
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, countBoundSql);
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt =connection.prepareStatement(sql);
            parameterHandler.setParameters(pstmt);
            rs = pstmt.executeQuery();
            if (rs.next()) {
                int totalRegistros = rs.getInt(1);
                paginacionVo.setTotalRegistros(totalRegistros);  // Total de registros
                paginacionVo.setTotalPaginas((totalRegistros-1)/paginacionVo.getTamanioPagina()+1); // Total de páginas
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null)
                    rs.close();
                if (pstmt != null)
                    pstmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
 
    /**
     * Obtener SQL paginado
     * @param sql SQL original interceptado
     * @param paginacionVo Objeto de parámetros de paginación
     * @return
     */
    private String obtenerSqlPaginado(String sql, PaginacionVO paginacionVo) {
        StringBuffer sqlBuffer = new StringBuffer(sql);
        if(dialecto.equalsIgnoreCase("mysql")){
            return this.obtenerSqlPaginadoMysql(sqlBuffer, paginacionVo);
        }else if(dialecto.equalsIgnoreCase("oracle")){
            return this.obtenerSqlPaginadoOracle(sqlBuffer, paginacionVo);
        }else {
            return sqlBuffer.toString();
        }
    }
 
    /**
     * Obtener SQL de conteo, calcular número total de registros
     * @param sql SQL original interceptado
     * @return
     */
    private String obtenerSqlConteo(String sql) {
        int beginIndex = sql.indexOf("from");
        sql = sql.substring(beginIndex);
        sql = "select count(1) " + sql;
        return sql;
    }
 
    /**
     * Obtener SQL paginado para MySQL
     * @param sql SQL original interceptado
     * @param paginacionVo Parámetros de paginación
     * @return Devuelve SQL paginado
     */
    private String obtenerSqlPaginadoMysql(StringBuffer sql, PaginacionVO paginacionVo) {
        sql.append(" limit ")
                .append(paginacionVo.getIndicePagina())
                .append(",")
                .append(paginacionVo.getTamanioPagina());
        return sql.toString();
    }
 
    /**
     * Obtener SQL paginado para Oracle
     * @param sql
     * @param paginacionVo
     * @return
     */
    private String obtenerSqlPaginadoOracle(StringBuffer sql, PaginacionVO paginacionVo) {
        int pagina = paginacionVo.getIndicePagina();
        int tamanio = paginacionVo.getTamanioPagina();
        // Calcular índices de inicio y fin de registros
        int indiceInicio = (pagina - 1) * tamanio;
        int indiceFin = pagina * tamanio;
        // Concatenar índice menor
        sql.insert(0, "select u.*, rownum r from (")
                .append(") u where rownum <= ")
                .append(indiceFin);
        // Concatenar índice mayor
        sql.insert(0, "select * from (")
                .append(") where r > ")
                .append(indiceInicio);
        return sql.toString();
    }
}

Creación del Interceptor de Resultados ResultInterceptor

package com.gitee.zhuyb.interceptor;
 
import com.gitee.zhuyb.dominio.PaginacionUtil;
import com.gitee.zhuyb.dominio.PaginacionVO;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
 
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

@Intercepts({@Signature(args = { Statement.class }, method = "handleResultSets", type = ResultSetHandler.class)})
public class ResultInterceptor implements Interceptor {
 
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        List resultado = new ArrayList();
        // Obtener objeto PaginacionVO
        PaginacionVO paginacionVo = PaginacionUtil.obtenerPaginacionVO();
        if (paginacionVo != null) {
            List<?> lista = (List<?>) invocation.proceed();
            // Establecer resultados de consulta en objeto PaginacionVO
            paginacionVo.setDatos(lista);
            resultado.add(paginacionVo);
        } else {
            resultado = (List)invocation.proceed();
        }
        return resultado;
    }
 
    @Override
    public Object plugin(Object target) {
        if(target instanceof ResultSetHandler){
            return Plugin.wrap(target, this);
        }else{
            return target;
        }
    }
 
    @Override
    public void setProperties(Properties properties) {
    }
}

Prueba del Efecto de Paginación

package com.gitee.zhuyb;
 
import com.gitee.zhuyb.dominio.PaginacionUtil;
import com.gitee.zhuyb.dominio.PaginacionVO;
import com.gitee.zhuyb.dominio.SysUser;
import com.gitee.zhuyb.mapeador.SysUserMapper;
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 java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class PruebaPaginacion {
    public static void main( String[] args ) throws IOException {
        // Leer archivo mybatis-config.xml
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        // Inicializar mybatis, crear instancia de SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // Crear instancia de Session
        SqlSession session = sqlSessionFactory.openSession();
        // Obtener objeto proxy de la interfaz mapper
        SysUserMapper sysUserMapper = session.getMapper(SysUserMapper.class);
 
        // Activar paginación
        PaginacionVO paginacionVo = new PaginacionVO(0, 2);
        PaginacionUtil.iniciar(paginacionVo);
 
        // Consultar base de datos
        SysUser sysUser = new SysUser();
        List<SysUser> listaUsuarios = sysUserMapper.consultarUsuario(sysUser);
 
        // Finalizar paginación
        PaginacionVO resultado = PaginacionUtil.finalizar();
        System.out.println("Resultado de paginación: " + resultado);
 
        // Confirmar transacción
        session.commit();
        // Cerrar Session
        session.close();
    }
}

Ejecutando la clase PruebaPaginacion, se obtiene el siguiente resultado:

Código del Proyecto

Configuración del Plugin Personalizado de MyBatis en Proyectos Spring Boot

  1. Usar @Bean para inyectar el Plugin persoanlizado

En Spring Boot, podemos usar el siguiente código para la inyección:

@Configuration 
public class ConfiguracionMyBatis{
    @Bean 
    public SQLStatsInterceptor interceptorEstadisticasSQL(){ 
        SQLStatsInterceptor interceptorEstadisticasSQL = new SQLStatsInterceptor(); 
        Properties properties= new Properties(); 
        properties.setProperty("dialecto","mysql"); 
        interceptorEstadisticasSQL.setProperties(properties); 
        return interceptorEstadisticasSQL;
    }
}

  1. En Spring, si se utiliza la configuración XML, se puede usar el siguiente método para la inyección:
<?xml version="1.0" encoding="UTF-8" ?>

<!-- Archivo de configuración XML con ajustes principales del sistema MyBatis -->
<configuration>
    <!-- Especificar archivo de configuración de base de datos -->
    <properties resource="db.properties" />
    <!-- Configurar plugin de paginación personalizado -->
    <plugins>
        <!-- Plugin de paginación personalizado -->
        <plugin interceptor="com.gitee.zhuyb.interceptor.PageInterceptor">
            <property name="dialecto" value="mysql"/>
        </plugin>
        <!-- Plugin para procesamiento de conjuntos de resultados -->
        <plugin interceptor="com.gitee.zhuyb.interceptor.ResultInterceptor">
        </plugin>
    </plugins>
    <!-- Configuración del entorno de base de datos -->
    <environments default="mysql">
        <!-- Configuración del entorno, es decir, la base de datos conectada -->
        <environment id="mysql">
            <!-- Tipo de gestión de transacciones, type="JDBC" indica un uso directo simple de la configuración de提交 y 回滚 de JDBC -->
            <transactionManager type="JDBC" />
            <!-- Configuración del dataSource, POOLED es la implementación de pool de conexiones del dataSource de objetos de conexión JDBC -->
            <dataSource type="POOLED">
                <property name="driver" value="${driver}" />
                <property name="url" value="${url}" />
                <property name="username" value="${username}" />
                <property name="password" value="${password}" />
            </dataSource>
        </environment>
    </environments>
    <!-- Configurar ruta de archivos de mapeo -->
    <mappers>
        <!-- Configurar ruta del archivo de mapeo UserMapper.xml -->
        <mapper resource="com/gitee/zhuyb/mapper/SysUserMapper.xml" />
    </mappers>
</configuration>

Referencias, sitio oficial

http://www.mybatis.org/mybatis-3/zh/configuraton.html#plugins

Etiquetas: MyBatis Paginación interceptor java Spring Boot

Publicado el 8-1 13:19