Solución al problema de nombres de columnas en minúsculas al usar HashMap en MyBatis

Al implementar consultas dinámicas en MyBatis donde las columnas se seleccionan en tiempo de ejecución, es una práctica común mapear los resultados directamente a un java.util.HashMap. Sin embargo, un problema frecuente es que los nombres de las claves (keys) en el mapa resultante se transforman automáticamente a minúsculas, perdiendo cualquier formato camelCase definido en los alias de SQL.

El escenario del problema

Consideremos una consulta dinámica donde los alias se generan para seguir una convención de nombres específica:

<select id="fetchDynamicData" resultType="java.util.HashMap">
    SELECT ${columnSelection}
    FROM inventory_table
    WHERE status = 'ACTIVE'
</select>

Si la varible columnSelection contiene algo como product_id AS productId, stock_count AS stockCount, esperaríamos un mapa como este:

{ "productId": 101, "stockCount": 50 }

No obstante, el resultado real suele ser:

{ "productid": 101, "stockcount": 50 }

¿Por qué sucede esto?

Este comportamiento se debe a la interacción de dos factores críticos:

  1. Comportamiento del motor de base de datos: Muchos sistemas de gestión de bases de datos (como PostgreSQL u Oracle) convierten automáticamente los identificadores (incluidos los alias) que no están entre comillas a minúsculas o mayúsculas por defecto.
  2. Limitación de la configuración de MyBatis: La propiedad mapUnderscoreToCamelCase de MyBatis solo se aplica cuando el resultado se mapea a un POJO (JavaBean). Cuando se utiliza un HashMap, MyBatis utiliza directamente los nombres de columna proporcionados por los metadatos del ResultSet de JDBC sin aplicar transformaciones adicionales.

Solución A: Uso de identificadores delimitados en SQL (Recomendado)

La forma más eficiente de resolver esto es forzar a la base de datos a respetar el uso de mayúsculas y minúsculas mediante comillas dobles en los alias. Esto garantiza que el ResultSet entregue los nombres exactos a MyBatis.

String selectClause = columns.stream()
    .map(col -> col.getSource() + " AS \"" + col.getAlias() + "\"")
    .collect(Collectors.joining(", "));

Al ejecutar una consulta como SELECT product_id AS "productId", el motor de base de datos preservará la cadena tal cual, y el HashMap de MyBatis contendrá la clave productId correctamente.

Ventajas:

  • Impacto nulo en el rendimiento (sin procesameinto adicional en Java).
  • Consistencia total con los requisitos del frontend o contratos de API.

Solución B: Post-procesamiento en la capa de aplicación

Si no es posible modificar la generación del SQL, se pueden transformar las claves del mapa una vez recuperados los datos. Aunque es funcional, este enfoque consume más recursos de CPU y memoria al crear nuevos objetos para cada fila.

public List<Map<String, Object>> transformKeysToCamelCase(List<Map<String, Object>> dataList) {
    return dataList.stream().map(originalMap -> {
        Map<String, Object> newMap = new HashMap<>(originalMap.size());
        originalMap.forEach((key, value) -> {
            String camelKey = convertSnakeToCamel(key); // Método de utilidad para conversión
            newMap.put(camelKey, value);
        });
        return newMap;
    }).collect(Collectors.toList());
}

Prevención de inyección SQL y errores de sintaxis

Al utilizar la interpolación de cadenas (${}) para los nombres de las columnas, es vital asegurarse de que los alias no rompan la estructura del SQL. Si un alias contiene comillas dobles, la consulta fallará.

Se recomienda implementar un método de escape para los identificadores:

private String escapeSqlIdentifier(String name) {
    if (name == null) return "null";
    // Escapar comillas dobles internas duplicándolas según el estándar SQL
    return "\"" + name.replace("\"", "\"\"") + "\"";
}

Al aplicar este método durante la construcción de la consulta dinámica, se garantiza que los alias complejos no comprometan la ejecución del comando SQL.

Riesgos de inconsistencia de datos

Es importante notar que si el alias en SQL (sin comillas) se convierte a productid pero el código que consume el mapa intenta acceder a data.get("productId"), obtendrá un valor nulo. Este es un error silencioso que puede causar que los reportes o interfaces muestren celdas vacías sin lanzar excepciones. Por ello, la consistencia entre la generación del alias y el acceso a los datos es fundamental para la integridad de la aplicación.

Etiquetas: MyBatis SQL java PostgreSQL JDBC

Publicado el 11-6 21:03