Mapeo de relaciones en MyBatis con anotaciones: uno a uno, uno a muchos y muchos a muchos

Dependencia del proyecto

<!-- Integración de Spring Boot con el framework MyBatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

Caso: Relación uno a uno

Clases de entidad:

// Clase Persona que referencia a la clase Tarjeta
@Data
@AllArgsConstructor
@NoArgsConstructor
// Esta anotación es obligatoria; de lo contrario, se producirá un error
@JsonIgnoreProperties(value = {"handler"})
public class Persona {
    private String nombre;
    private int id;
    private int edad;
    // Referencia a la clase Tarjeta
    private Tarjeta tarjeta;
}

// Clase Tarjeta
@Data
@AllArgsConstructor
@NoArgsConstructor
// Esta anotación es obligatoria; de lo contrario, se producirá un error
@JsonIgnoreProperties(value = {"handler"})
public class Tarjeta {
    // Clave externa que referencia al id de Persona
    private int usuarioId;
    private String numero;
}

Interfaz del Mapper:

// Interfaz del Mapper para Persona
@Mapper
@Repository
public interface PersonaMapper {
    // Definición de mapeo (para reutilización)
    // Configura el Mapper y método de la Tarjeta en Persona; establece el campo "usuarioId" de Tarjeta como id de Persona
    // one=@One indica que se mapea una entidad, es decir, relación uno a uno
    // FetchType.EAGER: carga inmediata; al cargar una entidad, los atributos definidos con carga inmediata se obtienen de la base de datos sin demora
    @Results(id="mapaPersona", value={
            @Result(id=true, column="id", property="id"),
            @Result(column="nombre", property="nombre"),
            @Result(column="edad", property="edad"),
            @Result(column="id", property="tarjeta",
                    one=@One(select="com.ejemplo.dao.TarjetaMapper.obtenerTarjetaPorUsuarioId", fetchType= FetchType.EAGER))
    })
    public Persona seleccionarUno();
    
    @Select("select * from persona where id = #{id}")
    // Referencia al mapeo definido anteriormente
    @ResultMap(value="mapaPersona")
    public Persona obtenerPersonaPorId(int id);
}

// Interfaz del Mapper para Tarjeta
@Mapper
@Repository
public interface TarjetaMapper {
    // usuarioId es la clave externa que referencia al campo id de Persona
    @Select("select * from tarjeta where usuario_id = #{usuarioId} ")
    public Tarjeta obtenerTarjetaPorUsuarioId(int usuarioId);
}

Prueba unitaria:

@Test   // Relación uno a uno
public void probarUnoAUno() throws IOException {
    // Crear conexión de datos
    SqlSessionFactory fabricaSesiones = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("configuracion-mybatis"));
    SqlSession sesion = fabricaSesiones.openSession();
    // Obtener la interfaz del Mapper
    PersonaMapper personaMapper = sesion.getMapper(PersonaMapper.class);
    // Consultar objeto Persona por id, obteniendo simultáneamente la Tarjeta asociada mediante la relación uno a uno
    Persona persona = personaMapper.obtenerPersonaPorId(1);
    // Imprimir resultados
    System.out.println(persona);
    // Verificar si la Tarjeta referenciada en Persona tiene valor
    System.out.println(persona.getTarjeta().getNumero());
    // Cerrar la conexión de datos
    sesion.close();
}

Caso: Relación uno a muchos

Clases de entidad:

// Clase Provincia que referencia a un conjunto de la clase Ciudad
@Data
@AllArgsConstructor
@NoArgsConstructor
// Esta anotación es obligatoria; de lo contrario, se producirá un error
@JsonIgnoreProperties(value = {"handler"})
public class Provincia {
    private String nombre;
    private int id;
    private int poblacion;
    // Referencia al conjunto de Ciudades
    private Set<Ciudad> ciudades;
}

// Clase Ciudad
@Data
@AllArgsConstructor
@NoArgsConstructor
// Esta anotación es obligatoria; de lo contrario, se producirá un error
@JsonIgnoreProperties(value = {"handler"})
public class Ciudad {
    private int ciudadId;
    private String nombreCiudad;
    // Clave externa que referencia al id de Provincia (en una relación uno a muchos, múltiples Ciudades corresponden a una Provincia)
    private int provinciaId;
}

Interfaz del Mapper:

// Interfaz del Mapper para Provincia
@Mapper
@Repository
public interface ProvinciaMapper {
    // Definición de mapeo (para reutilización)
    // Configura el Mapper y método de las Ciudades en Provincia; establece el campo "provinciaId" de Ciudad como id de Provincia
    // many=@Many indica que se mapean múltiples entidades, es decir, relación uno a muchos o muchos a muchos
    // FetchType.LAZY: carga diferida; al cargar una entidad, los atributos definidos con carga diferida no se obtienen de la base de datos inmediatamente
    @Results(id="mapaProvincia", value={
            @Result(id=true, column="id", property="id"),
            @Result(column="nombre", property="nombre"),
            @Result(column="id", property="ciudades",
                    many=@Many(select="com.ejemplo.dao.CiudadMapper.obtenerCiudadesPorProvinciaId", fetchType= FetchType.LAZY))
    })
    public Provincia seleccionarUno();
    
    @Select("select * from provincia where id = #{id}")
    // Referencia al mapeo definido anteriormente
    @ResultMap(value="mapaProvincia")
    public Provincia obtenerProvinciaPorId(int id);
}

// Interfaz del Mapper para Ciudad
@Mapper
@Repository
public interface CiudadMapper {
    // El campo provinciaId corresponde al id de Provincia
    @Select("select * from ciudad where provincia_id=#{provinciaId}")
    public List<Ciudad> obtenerCiudadesPorProvinciaId(int provinciaId);
}

Prueba unitaria:

@Test   // Relación uno a muchos
public void probarUnoAMuchos() throws IOException {
    // Establecer sesión de conexión SqlSession
    SqlSessionFactory fabricaSesiones = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("configuracion-mybatis"));
    SqlSession sesion = fabricaSesiones.openSession();
    // Obtener el Mapper
    ProvinciaMapper provinciaMapper = sesion.getMapper(ProvinciaMapper.class);
    // Invocar la interfaz
    Provincia provincia = provinciaMapper.obtenerProvinciaPorId(1);
    // Imprimir resultados
    System.out.println(provincia.getNombre());
    // Verificar si el conjunto de Ciudades tiene valores
    for (Ciudad ciudad : provincia.getCiudades()) {
        System.out.println(ciudad.getNombreCiudad());
    }
    // Cerrar la conexión
    sesion.close();
}

Caso: Relación muchos a muchos

Clases de entidad:

// Clase Usuario que referencia a un conjunto de la clase Rol
@Data
@AllArgsConstructor
@NoArgsConstructor
// Esta anotación es obligatoria; de lo contrario, se producirá un error
@JsonIgnoreProperties(value = {"handler"})
public class Usuario {
    private int id;
    private String nombre;
    // Conjunto de roles
    private Set<Rol> roles;
}

// Clase Rol que referencia a un conjunto de la clase Permiso
@Data
@AllArgsConstructor
@NoArgsConstructor
// Esta anotación es obligatoria; de lo contrario, se producirá un error
@JsonIgnoreProperties(value = {"handler"})
public class Rol {
    private int rolId;
    private String nombreRol;
    // Referencia al id de Usuario
    private int usuarioId;
    // Conjunto de permisos
    private Set<Permiso> permisos;
}

// Clase Permiso
@Data
@AllArgsConstructor
@NoArgsConstructor
// Esta anotación es obligatoria; de lo contrario, se producirá un error
@JsonIgnoreProperties(value = {"handler"})
public class Permiso {
    private int permisoId;
    private String nombrePermiso;
    // Referencia al campo rolId de Rol
    private int rolId;
}

Interfaz del Mapper:

// Interfaz del Mapper para Usuario
@Mapper
@Repository
public interface UsuarioMapper {
    // Consulta difusa para obtener un conjunto de usuarios
    @Select("select * from usuarios where nombre like %#{nombre}%")
    // Método abreviado para el mapeo
    @Results({
            @Result(id=true, column="id", property="id"),
            @Result(column="nombre", property="nombre"),
            @Result(column="id", property="roles",
                    many=@Many(select="com.ejemplo.dao.RolMapper.obtenerRolesPorUsuarioId", fetchType= FetchType.LAZY))
    })
    public List<Usuario> obtenerUsuariosPorNombre(String nombre);
}

// Interfaz del Mapper para Rol
@Mapper
@Repository
public interface RolMapper {
    // Obtener conjunto de roles según el usuarioId
    @Select("select * from roles where usuario_id = #{usuarioId}")
    // Método abreviado para el mapeo
    @Results({
            @Result(id=true, column="rol_id", property="rolId"),
            @Result(column="nombre_rol", property="nombreRol"),
            @Result(column="usuario_id", property="usuarioId"),
            @Result(column="rol_id", property="permisos",
                    many=@Many(select="com.ejemplo.dao.PermisoMapper.obtenerPermisosPorRolId", fetchType= FetchType.LAZY))
    })
    public List<Rol> obtenerRolesPorUsuarioId(int usuarioId);
}

// Interfaz del Mapper para Permiso
@Mapper
@Repository
public interface PermisoMapper {
    // Obtener conjunto de permisos según el rolId
    @Select("select * from permisos where rol_id = #{rolId}")
    // Método abreviado para el mapeo
    @Results({
            @Result(id=true, column="permiso_id", property="permisoId"),
            @Result(column="nombre_permiso", property="nombrePermiso"),
            @Result(column="rol_id", property="rolId")
    })
    public List<Permiso> obtenerPermisosPorRolId(int rolId);
}

Prueba unitaria:

@Test   // Relación muchos a muchos
public void probarMuchosAMuchos() throws IOException {
    // Establecer sesión de conexión SqlSession
    SqlSessionFactory fabricaSesiones = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("configuracion-mybatis"));
    SqlSession sesion = fabricaSesiones.openSession();
    // Obtener la interfaz UsuarioMapper
    UsuarioMapper usuarioMapper = sesion.getMapper(UsuarioMapper.class);
    // Invocar la interfaz
    List<Usuario> usuarios = usuarioMapper.obtenerUsuariosPorNombre("Ejemplo");
   
    // Imprimir resultados
    for (Usuario usuario : usuarios) {
        System.out.println(usuario.getNombre());
        // Verificar si el conjunto de Roles no está vacío
        for (Rol rol : usuario.getRoles()) {
            System.out.println(rol.getNombreRol());
            // Verificar si el conjunto de Permisos no está vacío
            for (Permiso permiso : rol.getPermisos()) {
                System.out.println(permiso.getNombrePermiso());
            }
        }
    }
    // Cerrar la conexión
    sesion.close();
}

Manejo de errores

Información del error:

No se encontró un serializador parra la clase org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory$EnhancedResultObjectProxyImpl. Esto ocurre al intentar convertir un objeto proxy a JSON.

Causa:

Durante consultas en cascada en MyBatis, se genera una propiedad "handler" en la estructura de datos del proxy.

Solución:

Agregar la anotación @JsonIgnoreProperties a las clases de entidad relacionadas para indicar que se ignoren ciertas propiedades al serializar a JSON.

@JsonIgnoreProperties(value = {"handler"})

Esta anotación le dice al componente de serialización JSON que ignore las propiedades en el array value, como "handler". Si se necesitan ignorar otras propiedades, como "hibernateLazyInitializer" o "fieldHandler", se pueden agregar al array.

Etiquetas: MyBatis Spring Boot java anotaciones ORM

Publicado el 6-15 02:37