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
- 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;
}
}
- 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