Diferencias clave y funcionamiento interno de MyBatis

En el desarrollo Java con persistencia, MyBatis es una herramienta fundamental. A continuación se explican conceptos esenciales sobre su uso y arquitectura interna.

¿Cuál es la diferencia entre #{} y ${}?

  • ${} realiza una sustitución textual directa del valor proporcionado. Se usa comúnmente para inyectar nombres de columnas, tablas o cláusulas dinámicas como en ORDER BY ${columna}. No ofrece protección contra inyección SQL.
  • #{} representa un parámetro preparado (PreparedStatement). MyBatis lo reemplaza por un ? y asigna el valor mediante métodos seguros como ps.setString(). Soporta acceso a propiedades anidadas mediante reflexión, como #{usuario.nombre}.

Etiquetas adicionales en archivos XML de mapeo

Además de <select>, <insert>, <update> y <delete>, MyBatis soporta:

  • <resultMap>: define mapeos complejos entre columnas y propiedades.
  • <sql> y <include>: permiten reutilizar fragmentos SQL.
  • <selectKey>: genera valores de clave primaria en bases que no soportan autoincremento.
  • Etiquetas de SQL dinámico: <if>, <choose>/<when>/<otherwise>, <where>, <set>, <trim>, <foreach>, <bind>.

Funcionamiento de las interfaces Mapper y sobrecarga de métodos

MyBatis utiliza proxies generados con JDK Dynamic Proxy. Cada método en una interfaz Mapper se resuelve mediante la combinación única de namespace + id, donde el namespace coincide con el nombre completo de la interfaz y el id con el nombre del método.

Aunque técnicamente se pueden definir métodos sobrecargados en la interfaz, todos deben compartir el mismo id en el XML. Esto funciona gracias al contexto de parámetros que MyBatis construye internamente:

public interface PersonaMapper {
    Persona buscar();
    Persona buscar(@Param("id") Long id);
    Persona buscar(@Param("id") Long id, @Param("nombre") String nombre);
}

El archivo XML asociado usa SQL dinámico para adaptarse a los distintos casos:

<select id="buscar" resultMap="PersonaMap">
  SELECT id, nombre, edad FROM persona
  <where>
    <if test="id != null">id = #{id}</if>
    <if test="nombre != null and nombre != ''">nombre = #{nombre}</if>
  </where>
  LIMIT 1
</select>

Internamente, MyBatis encapsula los parámetros en un mapa que incluye tanto los nombres explícitos (como id) como los implícitos (param1, param2, etc.). Si un parámetro referenciado en <if> no existe en el mapa, se lanza una excepción.

Paginación en MyBatis

Existen tres enfoques:

  1. Paginación en memoria: usando RowBounds, que filtra resultados tras ejecutar la consulta completa.
  2. Paginación física manual: escribiendo directamente cláusulas LIMIT o ROWNUM en el SQL.
  3. Plugins de paginación: interceptan la ejecución SQL y lo reescriben añadiendo la lógica de paginación según el dialecto de la base de datos (por ejemplo, envolviendo la consulta original en una subconsulta con LIMIT).

Arquitectura de plugins

MyBatis permite interceptar llamadas a cuatro interfaces clave: Executor, StatementHandler, ParameterHandler y ResultSetHandler. Para crear un plugin:

  1. Implementar org.apache.ibatis.plugin.Interceptor.
  2. Annotar con @Intercepts especificando la interfaz y métodos a interceptar.
  3. Registrar el plugin en el archivo de configuración de MyBatis.

El mecanismo se basa en proxies dinámicos que redirigen las invocaciones al método intercept() del plugin.

Mapeo de resultados

MyBatis soporta dos formas de mapear columnas a propiedades de objetos:

  1. Usando <resultMap>: define explícitamente cada correspondencia columna-propiedad.
  2. Usando alias en SQL: al escribir SELECT nombre_usuario AS nombre, MyBatis asocia automáticamente la columna con la propiedad nombre, ignorando mayúsculas/minúsculas.

Una vez establecido el mapeo, MyBatis crea instancias mediante reflexión y asigna los valores obtenidos del ResultSet.

Consultas relacionales y carga diferida

MyBatis soporta relaciones uno-a-uno y uno-a-muchos mediante:

  • Consultas anidadas: múltiples sentencias SQL ejecutadas bajo demanda.
  • Joins con desduplicación: una única consulta con JOIN, donde <id> dentro de <resultMap> identifica registros únicos para evitar duplicados en colecciones.

La carga diferida (lazy loading) está disponible para asociaciones y colecciones. Cuando se accede a una propiedad no cargada, MyBatis usa CGLIB para generar un proxy que ejecuta la consulta faltante justo a tiempo.

Ejecutores (Executors)

MyBatis dispone de tres tipos de ejecutores:

  • SimpleExecutor: abre y cierra un Statement por cada operación.
  • ReuseExecutor: reutiliza Statements cacheándolos por SQL.
  • BatchExecutor: acumula operaciones INSERT/UPDATE/DELETE y las envía en lote mediante addBatch() y executeBatch().

El tipo de ejecutor se puede especificar globalmente en la configuración o por sesión al crear un SqlSession.

Otros aspectos relevantes

  • Enums: se mapean implementando un TypeHandler personalizado que convierte entre el tipo Java y el tipo JDBC.
  • Orden de etiquetas <sql>: MyBatis realiza dos pasadas de análisis, por lo que los fragmentos referenciados con <include> pueden declararse antes o después de su uso.
  • Estructura interna: todo el XML se transforma en objetos como Configuration, MappedStatement, ResultMap y BoundSql.
  • Semi-automático: a diferencia de Hibernate, MyBatis requiere que el desarrollador escriba el SQL explícitamente, incluso para relaciones, lo que le da mayor control pero menos automatización.

Etiquetas: MyBatis SQL Dinámico JDBC ORM Java Persistence

Publicado el 8-16 23:09