4.Dto
Gestión de alumnos en una academia (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de alumnos en una academia utilizando Spring Boot, aplicando una arquitectura en capas (Controller, Service, Repository) y el uso de DTOs para la comunicación entre cliente y servidor.
La aplicación debe permitir la gestión completa de alumnos almacenados en base de datos mediante Spring Data JPA.
Funcionalidades a implementar
Crear un alumno a partir de un DTO de entrada (AlumnoCrearDto), validando que los campos nombre, apellidos, email y teléfono sean obligatorios y correctos.
Obtener el listado completo de alumnos registrados.
Obtener un alumno a partir de su identificador.
Filtrar alumnos por nombre, apellidos, email o teléfono, permitiendo búsquedas parciales e ignorando mayúsculas/minúsculas.
Actualizar completamente los datos de un alumno mediante una operación PUT.
Actualizar parcialmente los datos de un alumno mediante una operación PATCH, modificando únicamente los campos enviados.
Eliminar un alumno a partir de su identificador.
Utilizar DTOs de salida (AlumnoDto) para las respuestas de la API.
Implementar el mapeo entre entidad y DTO mediante MapStruct.
Gestionar la lógica de negocio en una capa de servicio independiente.
Lanzar una excepción cuando se intente operar con un alumno que no exista.
package es.pulsoft.spring_academia.controller;
import es.pulsoft.spring_academia.dto.AlumnoCrearDto;
import es.pulsoft.spring_academia.dto.AlumnoDto;
import es.pulsoft.spring_academia.dto.AlumnoPatchDto;
import es.pulsoft.spring_academia.service.AlumnoService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
// Controlador REST encargado de gestionar las operaciones relacionadas con los alumnos.
// Recibe las peticiones HTTP y delega la lógica de negocio en el servicio.
@RestController
@RequestMapping("/alumnos")
public class AlumnoController {
private final AlumnoService alumnoService;
public AlumnoController(AlumnoService alumnoService) {
this.alumnoService = alumnoService;
}
// Obtiene el listado completo de alumnos.
@GetMapping
public List<AlumnoDto> listarAlumnos() {
return alumnoService.listarAlumnos();
}
// Obtiene un alumno a partir de su identificador.
@GetMapping("/{id}")
public AlumnoDto buscarAlumnoId(@PathVariable Long id) {
return alumnoService.buscarAlumnoId(id);
}
// Filtra alumnos utilizando uno o varios criterios de búsqueda.
@GetMapping("/filtrar")
public ResponseEntity<List<AlumnoDto>> filtrarAlumnos(
@RequestParam(required = false) String nombre,
@RequestParam(required = false) String apellidos,
@RequestParam(required = false) String email,
@RequestParam(required = false) String telefono) {
return ResponseEntity.ok(alumnoService.filtrarAlumnos(nombre, apellidos, email, telefono));
}
// Crea un nuevo alumno.
@PostMapping
public ResponseEntity<AlumnoDto> agregarAlumno(@Valid @RequestBody AlumnoCrearDto dto) {
return ResponseEntity.ok(alumnoService.agregarAlumno(dto));
}
// Elimina un alumno por su identificador.
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminarAlumno(@PathVariable Long id) {
alumnoService.eliminarAlumno(id);
return ResponseEntity.noContent().build();
}
// Actualiza completamente los datos de un alumno.
@PutMapping("/{id}")
public ResponseEntity<AlumnoDto> actualizarAlumno(
@PathVariable Long id,
@Valid @RequestBody AlumnoCrearDto dto) {
return ResponseEntity.ok(alumnoService.actualizarAlumno(id, dto));
}
// Actualiza parcialmente los datos de un alumno.
@PatchMapping("/{id}")
public ResponseEntity<AlumnoDto> actualizarParcial(
@PathVariable Long id,
@Valid @RequestBody AlumnoPatchDto dto) {
return ResponseEntity.ok(alumnoService.actualizarParcial(id, dto));
}
}
package es.pulsoft.spring_academia.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.*;
// DTO utilizado para recibir los datos necesarios para crear un nuevo alumno.
// Incluye las validaciones de entrada antes de procesar la petición.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AlumnoCrearDto {
@NotBlank(message = "El nombre es obligatorio")
private String nombre;
@NotBlank(message = "Los apellidos son obligatorio")
private String apellidos;
@NotBlank(message = "El email es obligatorio")
@Email(message = "Formato de email invalido.")
private String email;
@NotBlank(message = "El telefono es obligatorio")
private String telefono;
}
package es.pulsoft.spring_academia.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
// DTO utilizado para devolver la información básica de un alumno.
// Se emplea en las respuestas de la API para evitar exponer la entidad completa.
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AlumnoDto {
private Long id;
private String nombre;
private String apellidos;
}
package es.pulsoft.spring_academia.mapper;
import es.pulsoft.spring_academia.dto.AlumnoCrearDto;
import es.pulsoft.spring_academia.dto.AlumnoDto;
import es.pulsoft.spring_academia.dto.AlumnoPatchDto;
import es.pulsoft.spring_academia.model.Alumno;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
// Mapper encargado de convertir entre la entidad Alumno y sus distintos DTO.
// La implementación es generada automáticamente por MapStruct.
@Mapper(componentModel = "spring")
public interface AlumnoMapper {
// Convierte un DTO de creación en una entidad Alumno.
Alumno toEntity(AlumnoCrearDto dto);
// Convierte una entidad Alumno en un DTO de respuesta.
AlumnoDto toDto(Alumno alumno);
// Actualiza parcialmente una entidad Alumno ignorando los campos nulos del DTO.
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Alumno alumno, AlumnoPatchDto dto);
}
package es.pulsoft.spring_academia.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
// Entidad que representa a un alumno de la academia.
// Almacena su información personal y las matrículas asociadas.
@Entity
@Table(name = "alumno")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Alumno {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "El nombre es obligatorio")
private String nombre;
@NotBlank(message = "Los apellidos son obligatorio")
private String apellidos;
@NotBlank(message = "El email es obligatorio")
@Email(message = "Formato de email no valido")
@Column(unique = true)
private String email;
@NotBlank(message = "El telefono es obligatorio")
@Size(min = 9, max = 15, message = "El telefono debe tener entre 9 y 15 caracteres")
private String telefono;
// Relación uno a muchos con las matrículas del alumno.
// Un alumno puede estar matriculado en varios cursos.
@OneToMany(mappedBy = "alumno")
private List<Matricula> matriculas = new ArrayList<>();
}
package es.pulsoft.spring_academia.dto;
import jakarta.validation.constraints.Email;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
// DTO utilizado para recibir los datos de una actualización parcial de un alumno.
// Solo se modifican los campos enviados en la petición.
@Getter
@Setter
@NoArgsConstructor
public class AlumnoPatchDto {
private String nombre;
private String apellidos;
@Email(message = "Formato de email invalido.")
private String email;
private String telefono;
}
package es.pulsoft.spring_academia.repository;
import es.pulsoft.spring_academia.model.Alumno;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
// Repositorio JPA encargado de la persistencia de la entidad Alumno.
// Proporciona operaciones CRUD básicas y consultas derivadas personalizadas.
public interface AlumnoRepository extends JpaRepository<Alumno, Long> {
// Búsqueda de alumnos por nombre ignorando mayúsculas/minúsculas y coincidencia parcial.
List<Alumno> findByNombreContainingIgnoreCase(String nombre);
// Búsqueda de alumnos por apellidos ignorando mayúsculas/minúsculas y coincidencia parcial.
List<Alumno> findByApellidosContainingIgnoreCase(String apellidos);
// Búsqueda de alumnos por email ignorando mayúsculas/minúsculas y coincidencia parcial.
List<Alumno> findByEmailContainingIgnoreCase(String email);
// Búsqueda de alumnos por teléfono ignorando mayúsculas/minúsculas y coincidencia parcial.
List<Alumno> findByTelefonoContainingIgnoreCase(String telefono);
}
package es.pulsoft.spring_academia.service;
import es.pulsoft.spring_academia.dto.AlumnoCrearDto;
import es.pulsoft.spring_academia.dto.AlumnoDto;
import es.pulsoft.spring_academia.dto.AlumnoPatchDto;
import es.pulsoft.spring_academia.mapper.AlumnoMapper;
import es.pulsoft.spring_academia.model.Alumno;
import es.pulsoft.spring_academia.repository.AlumnoRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
// Servicio que contiene la lógica de negocio relacionada con los alumnos.
// Actúa como capa intermedia entre el controlador, el repositorio y el mapper.
@Service
public class AlumnoService {
private final AlumnoRepository alumnoRepository;
private final AlumnoMapper alumnoMapper;
public AlumnoService(AlumnoRepository alumnoRepository, AlumnoMapper alumnoMapper) {
this.alumnoRepository = alumnoRepository;
this.alumnoMapper = alumnoMapper;
}
// Obtiene todos los alumnos registrados en el sistema.
public List<AlumnoDto> listarAlumnos() {
return transformarLista(alumnoRepository.findAll());
}
// Busca un alumno por su identificador.
// Lanza excepción 404 si no existe.
public AlumnoDto buscarAlumnoId(Long id) {
Alumno alumno = alumnoRepository.findById(id)
.orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND,
"Alumno no encontrado."));
return alumnoMapper.toDto(alumno);
}
// Filtra alumnos según los parámetros recibidos.
// Se aplica el primer filtro válido encontrado.
public List<AlumnoDto> filtrarAlumnos(String nombre, String apellidos, String email, String telefono) {
if (nombre != null && !nombre.isBlank()) {
return transformarLista(alumnoRepository.findByNombreContainingIgnoreCase(nombre));
}
if (apellidos != null && !apellidos.isBlank()) {
return transformarLista(alumnoRepository.findByApellidosContainingIgnoreCase(apellidos));
}
if (email != null && !email.isBlank()) {
return transformarLista(alumnoRepository.findByEmailContainingIgnoreCase(email));
}
if (telefono != null && !telefono.isBlank()) {
return transformarLista(alumnoRepository.findByTelefonoContainingIgnoreCase(telefono));
}
return transformarLista(alumnoRepository.findAll());
}
// Convierte una lista de entidades Alumno en DTOs de respuesta.
private List<AlumnoDto> transformarLista(List<Alumno> listaAlumnos) {
return listaAlumnos
.stream()
.map(alumnoMapper::toDto)
.toList();
}
// Crea un nuevo alumno en base al DTO recibido.
public AlumnoDto agregarAlumno(AlumnoCrearDto dto) {
Alumno alumno = alumnoMapper.toEntity(dto);
Alumno guardado = alumnoRepository.save(alumno);
return alumnoMapper.toDto(guardado);
}
// Elimina un alumno por su ID.
// Lanza excepción 404 si no existe.
public void eliminarAlumno(Long id) {
if (!alumnoRepository.existsById(id)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"Alumno no encontrado");
}
alumnoRepository.deleteById(id);
}
// Actualiza completamente un alumno existente.
public AlumnoDto actualizarAlumno(Long id, AlumnoCrearDto dto) {
Alumno alumno = alumnoRepository.findById(id)
.orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND,
"Alumno no encontrado"));
alumno.setNombre(dto.getNombre());
alumno.setApellidos(dto.getApellidos());
alumno.setEmail(dto.getEmail());
alumno.setTelefono(dto.getTelefono());
return alumnoMapper.toDto(alumnoRepository.save(alumno));
}
// Actualiza parcialmente un alumno (PATCH).
// Solo se modifican los campos no nulos del DTO.
public AlumnoDto actualizarParcial(Long id, AlumnoPatchDto dto) {
Alumno alumno = alumnoRepository.findById(id)
.orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND,
"Alumno no encontrado"));
alumnoMapper.update(alumno, dto);
return alumnoMapper.toDto(alumnoRepository.save(alumno));
}
}
Gestión de profesores en una academia (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de profesores en una academia utilizando Spring Boot, aplicando una arquitectura en capas (Controller, Service, Repository) y el uso de DTOs para la comunicación entre cliente y servidor.
La aplicación debe permitir la gestión completa de profesores almacenados en base de datos mediante Spring Data JPA.
Funcionalidades a implementar
Crear un profesor a partir de un DTO de entrada (ProfesorCrearDto), validando que los campos nombre, apellidos, email y especialidad sean obligatorios y correctos.
Obtener el listado completo de profesores registrados.
Obtener un profesor a partir de su identificador.
Filtrar profesores por nombre, apellidos, email o especialidad, permitiendo búsquedas parciales e ignorando mayúsculas/minúsculas.
Actualizar completamente los datos de un profesor mediante una operación PUT.
Actualizar parcialmente los datos de un profesor mediante una operación PATCH, modificando únicamente los campos enviados.
Eliminar un profesor a partir de su identificador.
Utilizar DTOs de salida (ProfesorDto) para las respuestas de la API.
Implementar el mapeo entre entidad y DTO mediante MapStruct.
Gestionar la lógica de negocio en una capa de servicio independiente.
Lanzar una excepción cuando se intente operar con un profesor que no exista.
Requisitos técnicos
Uso de Spring Boot.
Uso de Spring Data JPA.
Arquitectura en capas.
Uso de DTOs para entrada y salida.
Validaciones con Jakarta Validation.
Manejo de errores mediante excepciones HTTP.
// Controlador REST encargado de gestionar las operaciones relacionadas con los profesores.
// Expone endpoints para CRUD completo y filtrado, delegando la lógica de negocio al servicio.
package es.pulsoft.spring_academia.controller;
import es.pulsoft.spring_academia.dto.ProfesorCrearDto;
import es.pulsoft.spring_academia.dto.ProfesorDto;
import es.pulsoft.spring_academia.dto.ProfesorPatchDto;
import es.pulsoft.spring_academia.service.ProfesorService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/profesores")
public class ProfesorController {
private final ProfesorService profesorService;
public ProfesorController(ProfesorService profesorService) {
this.profesorService = profesorService;
}
// Obtiene el listado completo de profesores.
@GetMapping
public List<ProfesorDto> listarProfesores() {
return profesorService.listarProfesores();
}
// Obtiene un profesor por su identificador.
@GetMapping("/{id}")
public ProfesorDto buscarProfesorId(@PathVariable Long id) {
return profesorService.buscarPorId(id);
}
// Filtra profesores según los criterios proporcionados.
// Si no se envían filtros, devuelve todos los registros.
@GetMapping("/filtrar")
public ResponseEntity<List<ProfesorDto>> filtrarProfesores(
@RequestParam(required = false) String nombre,
@RequestParam(required = false) String apellidos,
@RequestParam(required = false) String email,
@RequestParam(required = false) String especialidad) {
return ResponseEntity.ok(
profesorService.filtrarProfesores(nombre, apellidos, email, especialidad)
);
}
// Crea un nuevo profesor en el sistema.
@PostMapping
public ResponseEntity<ProfesorDto> agregarProfesor(@Valid @RequestBody ProfesorCrearDto dto) {
return ResponseEntity.ok(profesorService.crearProfesor(dto));
}
// Elimina un profesor por su identificador.
@DeleteMapping("/{id}")
public ResponseEntity<Void> borrarProfesor(@PathVariable Long id) {
profesorService.borrarProfesor(id);
return ResponseEntity.noContent().build();
}
// Actualiza completamente los datos de un profesor.
@PutMapping("/{id}")
public ResponseEntity<ProfesorDto> actualizarProfesor(
@PathVariable Long id,
@Valid @RequestBody ProfesorCrearDto dto) {
return ResponseEntity.ok(profesorService.actualizarProfesor(id, dto));
}
// Actualiza parcialmente los datos de un profesor.
// Solo modifica los campos enviados en la petición.
@PatchMapping("/{id}")
public ResponseEntity<ProfesorDto> actualizacionParcial(
@PathVariable Long id,
@RequestBody ProfesorPatchDto dto) {
return ResponseEntity.ok(profesorService.actualizacionParcial(id, dto));
}
}
package es.pulsoft.spring_academia.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.*;
// DTO utilizado para recibir los datos necesarios para crear un profesor.
// Incluye validaciones de entrada para asegurar la integridad de los datos.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProfesorCrearDto {
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
@NotBlank(message = "Los apellidos son obligatorios.")
private String apellidos;
@NotBlank(message = "El email es obligatorio.")
@Email(message = "Formato de email no valido.")
private String email;
@NotBlank(message = "La especialidad es obligatoria.")
private String especialidad;
}
package es.pulsoft.spring_academia.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
// DTO utilizado para devolver la información básica de un profesor.
// Se emplea en las respuestas de la API para evitar exponer la entidad completa.
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProfesorDto {
private Long id;
private String nombre;
private String apellidos;
private String especialidad;
}
package es.pulsoft.spring_academia.mapper;
import es.pulsoft.spring_academia.dto.ProfesorCrearDto;
import es.pulsoft.spring_academia.dto.ProfesorDto;
import es.pulsoft.spring_academia.dto.ProfesorPatchDto;
import es.pulsoft.spring_academia.model.Profesor;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
// Mapper encargado de convertir entre la entidad Profesor y sus distintos DTOs.
// La implementación es generada automáticamente por MapStruct.
@Mapper(componentModel = "spring")
public interface ProfesorMapper {
// Convierte un DTO de creación en una entidad Profesor.
Profesor toEntity(ProfesorCrearDto dto);
// Convierte una entidad Profesor en un DTO de respuesta.
ProfesorDto toDto(Profesor profesor);
// Actualiza parcialmente una entidad Profesor ignorando los campos nulos del DTO.
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Profesor profesor, ProfesorPatchDto dto);
}
package es.pulsoft.spring_academia.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
// Entidad que representa a un profesor dentro de la academia.
// Almacena sus datos personales, especialidad y los cursos que imparte.
@Entity
@Table(name = "profesor")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Profesor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
@NotBlank(message = "El nombre es obligatorio.")
private String apellidos;
@NotBlank(message = "El nombre es obligatorio.")
@Email(message = "Formato de email no valido.")
@Column(unique = true)
private String email;
@NotBlank(message = "El nombre es obligatorio.")
private String especialidad;
// Relación uno a muchos con los cursos que imparte el profesor.
// Un profesor puede impartir varios cursos.
@OneToMany(mappedBy = "profesor")
private List<Curso> cursos = new ArrayList<>();
}
package es.pulsoft.spring_academia.dto;
import jakarta.validation.constraints.Email;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
// DTO utilizado para la actualización parcial de un profesor.
// Permite modificar únicamente los campos enviados en la petición.
@Getter
@Setter
@NoArgsConstructor
public class ProfesorPatchDto {
private String nombre;
private String apellidos;
@Email(message = "Formato de email no valido.")
private String email;
private String especialidad;
}
package es.pulsoft.spring_academia.repository;
import es.pulsoft.spring_academia.model.Profesor;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
// Repositorio JPA encargado de la persistencia de la entidad Profesor.
// Proporciona operaciones CRUD básicas y consultas derivadas personalizadas.
public interface ProfesorRepository extends JpaRepository<Profesor, Long> {
// Búsqueda de profesores por nombre con coincidencia parcial e ignorando mayúsculas/minúsculas.
List<Profesor> findByNombreContainingIgnoreCase(String nombre);
// Búsqueda de profesores por apellidos con coincidencia parcial e ignorando mayúsculas/minúsculas.
List<Profesor> findByApellidosContainingIgnoreCase(String apellidos);
// Búsqueda de profesores por email con coincidencia parcial e ignorando mayúsculas/minúsculas.
List<Profesor> findByEmailContainingIgnoreCase(String email);
// Búsqueda de profesores por especialidad con coincidencia parcial e ignorando mayúsculas/minúsculas.
List<Profesor> findByEspecialidadContainingIgnoreCase(String especialidad);
}
package es.pulsoft.spring_academia.service;
import es.pulsoft.spring_academia.dto.ProfesorCrearDto;
import es.pulsoft.spring_academia.dto.ProfesorDto;
import es.pulsoft.spring_academia.dto.ProfesorPatchDto;
import es.pulsoft.spring_academia.mapper.ProfesorMapper;
import es.pulsoft.spring_academia.model.Profesor;
import es.pulsoft.spring_academia.repository.ProfesorRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
// Servicio que contiene la lógica de negocio relacionada con los profesores.
// Actúa como capa intermedia entre el controlador, el repositorio y el mapper.
@Service
public class ProfesorService {
private final ProfesorRepository profesorRepository;
private final ProfesorMapper profesorMapper;
public ProfesorService(ProfesorRepository profesorRepository, ProfesorMapper profesorMapper) {
this.profesorRepository = profesorRepository;
this.profesorMapper = profesorMapper;
}
// Obtiene todos los profesores registrados y los convierte a DTO de salida.
public List<ProfesorDto> listarProfesores() {
return convertirProfesores(profesorRepository.findAll());
}
// Busca un profesor por su identificador.
// Lanza excepción 404 si no existe.
public ProfesorDto buscarPorId(Long id) {
Profesor profesor = profesorRepository.findById(id)
.orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND,
"Profesor no encontrado"));
return profesorMapper.toDto(profesor);
}
// Filtra profesores según el primer criterio válido recibido.
// Si no se envía ningún filtro, devuelve todos los profesores.
public List<ProfesorDto> filtrarProfesores(String nombre, String apellidos, String email, String especialidad) {
if (nombre != null && !nombre.isBlank()) {
return convertirProfesores(profesorRepository.findByNombreContainingIgnoreCase(nombre));
}
if (apellidos != null && !apellidos.isBlank()) {
return convertirProfesores(profesorRepository.findByApellidosContainingIgnoreCase(apellidos));
}
if (email != null && !email.isBlank()) {
return convertirProfesores(profesorRepository.findByEmailContainingIgnoreCase(email));
}
if (especialidad != null && !especialidad.isBlank()) {
return convertirProfesores(profesorRepository.findByEspecialidadContainingIgnoreCase(especialidad));
}
return convertirProfesores(profesorRepository.findAll());
}
// Convierte una lista de entidades Profesor en DTOs de salida.
private List<ProfesorDto> convertirProfesores(List<Profesor> listaProfesores) {
return listaProfesores
.stream()
.map(profesorMapper::toDto)
.toList();
}
// Crea un nuevo profesor a partir del DTO recibido.
public ProfesorDto crearProfesor(ProfesorCrearDto dto) {
Profesor profesor = profesorMapper.toEntity(dto);
Profesor guardado = profesorRepository.save(profesor);
return profesorMapper.toDto(guardado);
}
// Elimina un profesor por su identificador.
// Lanza excepción 404 si el profesor no existe.
public void borrarProfesor(Long id) {
if (!profesorRepository.existsById(id)) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Profesor no encontrado");
}
profesorRepository.deleteById(id);
}
// Actualiza completamente los datos de un profesor existente.
public ProfesorDto actualizarProfesor(Long id, ProfesorCrearDto dto) {
Profesor profesor = profesorRepository.findById(id)
.orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND,
"Profesor no encontrado"));
profesor.setNombre(dto.getNombre());
profesor.setApellidos(dto.getApellidos());
profesor.setEspecialidad(dto.getEspecialidad());
Profesor guardado = profesorRepository.save(profesor);
return profesorMapper.toDto(guardado);
}
// Actualiza parcialmente un profesor (PATCH).
// Solo modifica los campos no nulos del DTO.
public ProfesorDto actualizacionParcial(Long id, ProfesorPatchDto dto) {
Profesor profesor = profesorRepository.findById(id)
.orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND,
"Profesor no encontrado"));
profesorMapper.update(profesor, dto);
Profesor guardado = profesorRepository.save(profesor);
return profesorMapper.toDto(guardado);
}
}
Gestión de cursos en una academia (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de cursos en una academia utilizando Spring Boot, aplicando una arquitectura en capas (Controller, Service, Repository) y el uso de DTOs para la comunicación entre cliente y servidor.
La aplicación debe permitir la gestión completa de cursos almacenados en base de datos mediante Spring Data JPA, incluyendo su relación con los profesores.
Funcionalidades a implementar
Crear un curso a partir de un DTO de entrada (CursoCrearDto), validando que los campos nombre, descripción, horas y nivel sean obligatorios y correctos.
Asociar cada curso a un profesor existente mediante su identificador.
Obtener el listado completo de cursos registrados.
Obtener un curso a partir de su identificador.
Filtrar cursos por nombre, descripción, número de horas, nivel o profesor, permitiendo búsquedas flexibles según los parámetros enviados.
Actualizar completamente los datos de un curso mediante una operación PUT.
Actualizar parcialmente los datos de un curso mediante una operación PATCH, modificando únicamente los campos enviados.
Eliminar un curso a partir de su identificador.
Utilizar DTOs de salida (CursoDto) para las respuestas de la API, incluyendo la referencia al profesor mediante su ID.
Implementar el mapeo entre entidad y DTO mediante MapStruct.
Gestionar la lógica de negocio en una capa de servicio independiente.
Lanzar una excepción cuando se intente operar con un curso que no exista.
Requisitos técnicos
Uso de Spring Boot.
Uso de Spring Data JPA.
Arquitectura en capas.
Uso de DTOs para entrada y salida.
Validaciones con Jakarta Validation.
Manejo de errores mediante excepciones HTTP.
Gestión de relaciones entre entidades (Curso ↔ Profesor).
// Controlador REST encargado de gestionar las operaciones relacionadas con los cursos.
// Expone endpoints para CRUD completo, filtrado y actualización parcial,
// delegando la lógica de negocio al servicio.
package es.pulsoft.spring_academia.controller;
import es.pulsoft.spring_academia.dto.CursoCrearDto;
import es.pulsoft.spring_academia.dto.CursoDto;
import es.pulsoft.spring_academia.dto.CursoPatchDto;
import es.pulsoft.spring_academia.enums.NivelEnum;
import es.pulsoft.spring_academia.service.CursoService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/cursos")
public class CursoController {
private final CursoService cursoService;
public CursoController(CursoService cursoService) {
this.cursoService = cursoService;
}
// Obtiene el listado completo de cursos.
@GetMapping
public List<CursoDto> listarCursos() {
return cursoService.listarCursos();
}
// Obtiene un curso por su identificador.
@GetMapping("/{id}")
public ResponseEntity<CursoDto> buscarPorId(@PathVariable Long id) {
return ResponseEntity.ok(cursoService.buscarPorId(id));
}
// Filtra cursos según los criterios proporcionados.
// Permite combinar distintos parámetros de búsqueda.
@GetMapping("/filtrar")
public ResponseEntity<List<CursoDto>> filtrarCursos(
@RequestParam(required = false) String nombre,
@RequestParam(required = false) String descripcion,
@RequestParam(required = false) Integer horas,
@RequestParam(required = false) NivelEnum nivel,
@RequestParam(required = false) Long profesorId) {
return ResponseEntity.ok(
cursoService.filtrarCursos(nombre, descripcion, horas, nivel, profesorId)
);
}
// Crea un nuevo curso en el sistema.
@PostMapping
public ResponseEntity<CursoDto> agregarCurso(@Valid @RequestBody CursoCrearDto dto) {
return ResponseEntity.ok(cursoService.agregarCurso(dto));
}
// Elimina un curso por su identificador.
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminarCurso(@PathVariable Long id) {
cursoService.eliminarCurso(id);
return ResponseEntity.noContent().build();
}
// Actualiza completamente los datos de un curso.
@PutMapping("/{id}")
public ResponseEntity<CursoDto> actualizarCurso(
@PathVariable Long id,
@Valid @RequestBody CursoCrearDto dto) {
return ResponseEntity.ok(cursoService.actualizarCurso(id, dto));
}
// Actualiza parcialmente los datos de un curso.
// Solo modifica los campos enviados en la petición.
@PatchMapping("/{id}")
public ResponseEntity<CursoDto> actualizacionParcial(
@PathVariable Long id,
@RequestBody CursoPatchDto dto) {
return ResponseEntity.ok(cursoService.actualizacionParcial(id, dto));
}
}
package es.pulsoft.spring_academia.dto;
import es.pulsoft.spring_academia.enums.NivelEnum;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.*;
// DTO utilizado para recibir los datos necesarios para la creación de un curso.
// Incluye validaciones para asegurar la integridad de los datos de entrada.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CursoCrearDto {
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
@NotBlank(message = "La descripcion es obligatoria.")
private String descripcion;
@NotNull(message = "Las horas son obligatorias.")
@Min(value = 1, message = "No puede ser menos de 1 hora.")
private Integer horas;
@NotNull(message = "El nivel es obligatorio.")
private NivelEnum nivel;
}
package es.pulsoft.spring_academia.dto;
import es.pulsoft.spring_academia.enums.NivelEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
// DTO utilizado para devolver la información de un curso en la API.
// Incluye los datos principales del curso y la referencia al profesor mediante su ID.
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CursoDto {
private Long id;
private String nombre;
private String descripcion;
private Integer horas;
private NivelEnum nivel;
private Long profesorId;
}
package es.pulsoft.spring_academia.mapper;
import es.pulsoft.spring_academia.dto.CursoCrearDto;
import es.pulsoft.spring_academia.dto.CursoDto;
import es.pulsoft.spring_academia.dto.CursoPatchDto;
import es.pulsoft.spring_academia.model.Curso;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
// Mapper encargado de convertir entre la entidad Curso y sus distintos DTOs.
// La implementación es generada automáticamente por MapStruct.
@Mapper(componentModel = "spring")
public interface CursoMapper {
// Convierte un DTO de creación en una entidad Curso.
Curso toEntity(CursoCrearDto dto);
// Convierte una entidad Curso en un DTO de respuesta.
CursoDto toDto(Curso curso);
// Actualiza parcialmente una entidad Curso ignorando los campos nulos del DTO.
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Curso curso, CursoPatchDto dto);
}
package es.pulsoft.spring_academia.model;
import es.pulsoft.spring_academia.enums.NivelEnum;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
// Entidad que representa un curso dentro de la academia.
// Contiene información del curso, su nivel, horas y su relación con profesor y matrículas.
@Entity
@Table(name = "curso")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Curso {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
@NotBlank(message = "La descripcion es obligatorio.")
private String descripcion;
@NotNull(message = "Las horas son obligatorio.")
private Integer horas;
// Nivel del curso (BÁSICO, INTERMEDIO, AVANZADO, etc.).
@NotNull(message = "El nivel es obligatorio.")
@Enumerated(EnumType.STRING)
private NivelEnum nivel;
// Relación muchos a uno con profesor.
// Cada curso está asignado a un único profesor.
@ManyToOne
@JoinColumn(name = "profesor_id")
@NotNull(message = "El profesor es obligatorio.")
private Profesor profesor;
// Relación uno a muchos con matrículas.
// Un curso puede tener múltiples alumnos matriculados.
@OneToMany(mappedBy = "curso")
private List<Matricula> matriculas = new ArrayList<>();
}
package es.pulsoft.spring_academia.dto;
import es.pulsoft.spring_academia.enums.NivelEnum;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
// DTO utilizado para la actualización parcial de un curso.
// Permite modificar únicamente los campos enviados en la petición.
@Getter
@Setter
@NoArgsConstructor
public class CursoPatchDto {
private String nombre;
private String descripcion;
private Integer horas;
private NivelEnum nivel;
}
package es.pulsoft.spring_academia.repository;
import es.pulsoft.spring_academia.enums.NivelEnum;
import es.pulsoft.spring_academia.model.Curso;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
// Repositorio JPA encargado de la persistencia de la entidad Curso.
// Proporciona operaciones CRUD básicas y consultas derivadas personalizadas.
public interface CursoRepository extends JpaRepository<Curso, Long> {
// Búsqueda de cursos por nombre con coincidencia parcial e ignorando mayúsculas/minúsculas.
List<Curso> findByNombreContainingIgnoreCase(String nombre);
// Búsqueda de cursos por descripción con coincidencia parcial e ignorando mayúsculas/minúsculas.
List<Curso> findByDescripcionContainingIgnoreCase(String descripcion);
// Búsqueda de cursos por número exacto de horas.
List<Curso> findByHoras(Integer horas);
// Búsqueda de cursos por nivel (BÁSICO, INTERMEDIO, AVANZADO, etc.).
List<Curso> findByNivel(NivelEnum nivel);
// Búsqueda de cursos por identificador del profesor asociado.
List<Curso> findByProfesorId(Long profesorId);
}
package es.pulsoft.spring_academia.service;
import es.pulsoft.spring_academia.dto.CursoCrearDto;
import es.pulsoft.spring_academia.dto.CursoDto;
import es.pulsoft.spring_academia.dto.CursoPatchDto;
import es.pulsoft.spring_academia.enums.NivelEnum;
import es.pulsoft.spring_academia.mapper.CursoMapper;
import es.pulsoft.spring_academia.model.Curso;
import es.pulsoft.spring_academia.repository.CursoRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
// Servicio que contiene la lógica de negocio relacionada con los cursos.
// Actúa como capa intermedia entre el controlador, el repositorio y el mapper.
@Service
public class CursoService {
private final CursoRepository cursoRepository;
private final CursoMapper cursoMapper;
public CursoService(CursoRepository cursoRepository, CursoMapper cursoMapper) {
this.cursoRepository = cursoRepository;
this.cursoMapper = cursoMapper;
}
// Convierte una lista de entidades Curso en DTOs de salida.
private List<CursoDto> convertirListaCurso(List<Curso> listaCursos) {
return listaCursos
.stream()
.map(cursoMapper::toDto)
.toList();
}
// Obtiene todos los cursos registrados.
public List<CursoDto> listarCursos() {
return convertirListaCurso(cursoRepository.findAll());
}
// Obtiene una entidad Curso por su ID o lanza excepción si no existe.
private Curso convertirCurso(Long id) {
return cursoRepository.findById(id)
.orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND,
"Curso no encontrado"));
}
// Busca un curso por su identificador.
public CursoDto buscarPorId(Long id) {
return cursoMapper.toDto(convertirCurso(id));
}
// Filtra cursos según el primer criterio válido recibido.
// Si no se envía ningún filtro, devuelve todos los cursos.
public List<CursoDto> filtrarCursos(String nombre, String descripcion, Integer horas, NivelEnum nivel, Long profesorId) {
if (nombre != null && !nombre.isBlank()) {
return convertirListaCurso(cursoRepository.findByNombreContainingIgnoreCase(nombre));
}
if (descripcion != null && !descripcion.isBlank()) {
return convertirListaCurso(cursoRepository.findByDescripcionContainingIgnoreCase(descripcion));
}
if (horas != null) {
return convertirListaCurso(cursoRepository.findByHoras(horas));
}
if (nivel != null) {
return convertirListaCurso(cursoRepository.findByNivel(nivel));
}
if (profesorId != null) {
return convertirListaCurso(cursoRepository.findByProfesorId(profesorId));
}
return convertirListaCurso(cursoRepository.findAll());
}
// Crea un nuevo curso en el sistema.
public CursoDto agregarCurso(CursoCrearDto dto) {
Curso curso = cursoMapper.toEntity(dto);
return cursoMapper.toDto(cursoRepository.save(curso));
}
// Elimina un curso por su ID.
// Lanza excepción 404 si no existe.
public void eliminarCurso(Long id) {
if (!cursoRepository.existsById(id)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"Curso no encontrado.");
}
cursoRepository.deleteById(id);
}
// Actualiza completamente un curso existente.
public CursoDto actualizarCurso(Long id, CursoCrearDto dto) {
Curso curso = convertirCurso(id);
curso.setNombre(dto.getNombre());
curso.setDescripcion(dto.getDescripcion());
curso.setNivel(dto.getNivel());
curso.setHoras(dto.getHoras());
return cursoMapper.toDto(cursoRepository.save(curso));
}
// Actualiza parcialmente un curso (PATCH).
// Solo modifica los campos no nulos del DTO.
public CursoDto actualizacionParcial(Long id, CursoPatchDto dto) {
Curso curso = convertirCurso(id);
cursoMapper.update(curso, dto);
return cursoMapper.toDto(cursoRepository.save(curso));
}
}
Gestión de matrículas en una academia (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de matrículas en una academia utilizando Spring Boot, aplicando una arquitectura en capas (Controller, Service, Repository) y el uso de DTOs para la comunicación entre cliente y servidor.
La aplicación debe permitir gestionar la relación entre alumnos y cursos, incluyendo información adicional como la fecha de matrícula y la nota final obtenida.
Funcionalidades a implementar
Crear una matrícula a partir de un DTO de entrada (MatriculaCrearDto), validando que el alumno, el curso y la fecha de matrícula sean obligatorios y correctos.
Garantizar que un alumno no pueda matricularse más de una vez en el mismo curso.
Obtener el listado completo de matrículas registradas.
Obtener una matrícula a partir de su identificador.
Filtrar matrículas por alumno, curso, fecha de matrícula o nota final.
Actualizar parcialmente una matrícula existente, permitiendo modificar la fecha de matrícula (y otros campos si se amplía la lógica).
Eliminar una matrícula a partir de su identificador.
Utilizar DTOs de salida (MatriculaDto) para las respuestas de la API.
Implementar el mapeo entre entidad y DTO mediante MapStruct.
Gestionar la lógica de negocio en una capa de servicio independiente.
Lanzar una excepción cuando se intente operar con una matrícula que no exista.
Requisitos técnicos
Uso de Spring Boot.
Uso de Spring Data JPA.
Arquitectura en capas.
Uso de DTOs para entrada y salida.
Validaciones con Jakarta Validation.
Manejo de errores mediante excepciones HTTP.
Gestión de relaciones entre entidades (Alumno ↔ Curso a través de Matricula).
Uso de restricciones de base de datos para evitar duplicados (alumno_id + curso_id).
// Controlador REST encargado de gestionar las operaciones relacionadas con las matrículas.
// Expone endpoints para CRUD básico y filtrado, delegando la lógica de negocio al servicio.
package es.pulsoft.spring_academia.controller;
import es.pulsoft.spring_academia.dto.MatriculaCrearDto;
import es.pulsoft.spring_academia.dto.MatriculaDto;
import es.pulsoft.spring_academia.service.MatriculaService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/matriculas")
public class MatriculaController {
private final MatriculaService matriculaService;
public MatriculaController(MatriculaService matriculaService) {
this.matriculaService = matriculaService;
}
// Obtiene el listado completo de matrículas.
@GetMapping
public List<MatriculaDto> listarMatriculas() {
return matriculaService.listarMatriculas();
}
// Obtiene una matrícula por su identificador.
@GetMapping("/{id}")
public MatriculaDto buscarmatriculaId(@PathVariable Long id) {
return matriculaService.buscarPorId(id);
}
// Filtra matrículas según los parámetros enviados.
// Permite búsqueda por alumno, curso, fecha o nota final.
@GetMapping("/filtrar")
public ResponseEntity<List<MatriculaDto>> filtrarMatriculas(
@RequestParam(required = false) Long alumnoId,
@RequestParam(required = false) Long cursoId,
@RequestParam(required = false) LocalDate fechaMatricula,
@RequestParam(required = false) BigDecimal notaFinal) {
return ResponseEntity.ok(
matriculaService.filtrarMatriculas(alumnoId, cursoId, fechaMatricula, notaFinal)
);
}
// Crea una nueva matrícula.
@PostMapping
public ResponseEntity<MatriculaDto> agregarMatricula(@Valid @RequestBody MatriculaCrearDto dto) {
return ResponseEntity.ok(matriculaService.agregarMatricula(dto));
}
// Elimina una matrícula por su identificador.
@DeleteMapping("/{id}")
public ResponseEntity<Void> borrarMatricula(@PathVariable Long id) {
matriculaService.borrarMatricula(id);
return ResponseEntity.noContent().build();
}
// Actualiza completamente una matrícula existente.
@PutMapping("/{id}")
public ResponseEntity<MatriculaDto> actualizarMatricula(
@PathVariable Long id,
@Valid @RequestBody MatriculaCrearDto dto) {
return ResponseEntity.ok(matriculaService.actualizarMatricula(id, dto));
}
}
package es.pulsoft.spring_academia.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import lombok.*;
import java.time.LocalDate;
// DTO utilizado para la creación de una matrícula.
// Recibe los identificadores de alumno y curso junto con la fecha de matrícula.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MatriculaCrearDto {
@NotNull(message = "El alumno es obligatorio.")
private Long alumnoId;
@NotNull(message = "El curso es obligatorio.")
private Long cursoId;
@NotNull(message = "La fecha es obligatoria.")
@PastOrPresent(message = "La fecha no puede ser mayor que el día actual.")
private LocalDate fechaMatricula;
}
package es.pulsoft.spring_academia.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
// DTO utilizado para devolver la información de una matrícula en la API.
// Representa la relación entre alumno y curso mediante sus identificadores.
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MatriculaDto {
private Long id;
private Long alumnoId;
private Long cursoId;
private LocalDate fechaMatricula;
}
package es.pulsoft.spring_academia.mapper;
import es.pulsoft.spring_academia.dto.MatriculaCrearDto;
import es.pulsoft.spring_academia.dto.MatriculaDto;
import es.pulsoft.spring_academia.dto.MatriculaPatchDto;
import es.pulsoft.spring_academia.model.Matricula;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
// Mapper encargado de convertir entre la entidad Matricula y sus distintos DTOs.
// MapStruct genera la implementación automáticamente en tiempo de compilación.
@Mapper(componentModel = "spring")
public interface MatriculaMapper {
// Convierte un DTO de creación en una entidad Matricula.
Matricula toEntity(MatriculaCrearDto dto);
// Convierte una entidad Matricula en un DTO de salida.
MatriculaDto toDto(Matricula matricula);
// Actualización parcial de una entidad Matricula.
// Solo modifica los campos no nulos del DTO recibido.
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Matricula matricula, MatriculaPatchDto dto);
}
package es.pulsoft.spring_academia.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDate;
// Entidad que representa la matrícula de un alumno en un curso.
// Gestiona la relación entre alumno y curso, incluyendo fecha y nota final.
@Entity
@Table(
name = "matricula",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"alumno_id", "curso_id"})
}
)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Matricula {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Relación muchos a uno con Alumno.
// Un alumno puede tener varias matrículas.
@ManyToOne
@JoinColumn(name = "alumno_id")
@NotNull(message = "El alumno es obligatorio.")
private Alumno alumno;
// Relación muchos a uno con Curso.
// Un curso puede tener varias matrículas de alumnos.
@ManyToOne
@JoinColumn(name = "curso_id")
@NotNull(message = "El curso es obligatorio.")
private Curso curso;
// Fecha en la que se realiza la matrícula.
// No puede ser una fecha futura.
@NotNull(message = "La fecha es obligatoria.")
@PastOrPresent(message = "La fecha de matricula no puede ser futura.")
private LocalDate fechaMatricula;
// Nota final del alumno en el curso.
// Debe estar entre 0 y 10.
@NotNull(message = "La nota es obligatoria.")
@DecimalMin(value = "0.00", message = "La nota no puede ser inferior a 0.")
@DecimalMax(value = "10.00", message = "La nota no puede ser superior a 10.")
private BigDecimal notaFinal;
}
package es.pulsoft.spring_academia.dto;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.PastOrPresent;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDate;
// DTO utilizado para la actualización parcial de una matrícula.
// Permite modificar únicamente los campos enviados en la petición.
@Getter
@Setter
@NoArgsConstructor
public class MatriculaPatchDto {
private Long alumnoId;
private Long cursoId;
@PastOrPresent(message = "La fecha no puede ser mayor que el día actual.")
private LocalDate fechaMatricula;
@DecimalMin(value = "0.00", message = "La nota no puede ser menor de 0.")
@DecimalMax(value = "10.00", message = "La nota no puede ser mayor de 10.")
private BigDecimal notaFinal;
}
package es.pulsoft.spring_academia.repository;
import es.pulsoft.spring_academia.model.Matricula;
import org.springframework.data.jpa.repository.JpaRepository;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
// Repositorio JPA encargado de la persistencia de la entidad Matricula.
// Proporciona operaciones CRUD básicas y consultas derivadas personalizadas.
public interface MatriculaRepository extends JpaRepository<Matricula, Long> {
// Obtiene matrículas filtrando por el identificador del alumno.
List<Matricula> findByAlumnoId(Long alumnoId);
// Obtiene matrículas filtrando por el identificador del curso.
List<Matricula> findByCursoId(Long cursoId);
// Obtiene matrículas filtrando por la fecha de matrícula.
List<Matricula> findByFechaMatricula(LocalDate fechaMatricula);
// Obtiene matrículas filtrando por la nota final exacta.
List<Matricula> findByNotaFinal(BigDecimal notaFinal);
}
package es.pulsoft.spring_academia.service;
import es.pulsoft.spring_academia.dto.MatriculaCrearDto;
import es.pulsoft.spring_academia.dto.MatriculaDto;
import es.pulsoft.spring_academia.mapper.MatriculaMapper;
import es.pulsoft.spring_academia.model.Matricula;
import es.pulsoft.spring_academia.repository.MatriculaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
// Servicio que contiene la lógica de negocio relacionada con las matrículas.
// Actúa como capa intermedia entre el controlador, el repositorio y el mapper.
@Service
public class MatriculaService {
private final MatriculaRepository matriculaRepository;
private final MatriculaMapper matriculaMapper;
public MatriculaService(MatriculaRepository matriculaRepository, MatriculaMapper matriculaMapper) {
this.matriculaRepository = matriculaRepository;
this.matriculaMapper = matriculaMapper;
}
// Convierte una lista de entidades Matricula en DTOs de salida.
private List<MatriculaDto> convertirMatriculas(List<Matricula> listaMatriculas) {
return listaMatriculas
.stream()
.map(matriculaMapper::toDto)
.toList();
}
// Obtiene todas las matrículas registradas.
public List<MatriculaDto> listarMatriculas() {
return convertirMatriculas(matriculaRepository.findAll());
}
// Obtiene una matrícula por su ID o lanza excepción si no existe.
public MatriculaDto buscarPorId(Long id) {
Matricula matricula = crearMatricula(id);
return matriculaMapper.toDto(matricula);
}
// Filtra matrículas según el primer criterio válido recibido.
// Si no se envía ningún filtro, devuelve todas las matrículas.
public List<MatriculaDto> filtrarMatriculas(Long alumnoId, Long cursoId, LocalDate fechaMatricula, BigDecimal notaFinal) {
if (alumnoId != null) {
return convertirMatriculas(matriculaRepository.findByAlumnoId(alumnoId));
}
if (cursoId != null) {
return convertirMatriculas(matriculaRepository.findByCursoId(cursoId));
}
if (fechaMatricula != null) {
return convertirMatriculas(matriculaRepository.findByFechaMatricula(fechaMatricula));
}
if (notaFinal != null) {
return convertirMatriculas(matriculaRepository.findByNotaFinal(notaFinal));
}
return convertirMatriculas(matriculaRepository.findAll());
}
// Crea una nueva matrícula en el sistema.
public MatriculaDto agregarMatricula(MatriculaCrearDto dto) {
Matricula matricula = matriculaMapper.toEntity(dto);
return matriculaMapper.toDto(matriculaRepository.save(matricula));
}
// Elimina una matrícula por su ID.
// Lanza excepción 404 si no existe.
public void borrarMatricula(Long id) {
if (!matriculaRepository.existsById(id)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"La matricula no existe");
}
matriculaRepository.deleteById(id);
}
// Actualiza parcialmente una matrícula existente.
// En este caso solo se modifica la fecha de matrícula.
public MatriculaDto actualizarMatricula(Long id, MatriculaCrearDto dto) {
Matricula matricula = crearMatricula(id);
matricula.setFechaMatricula(dto.getFechaMatricula());
return matriculaMapper.toDto(matriculaRepository.save(matricula));
}
// Método auxiliar para obtener una matrícula o lanzar excepción si no existe.
private Matricula crearMatricula(Long id) {
return matriculaRepository.findById(id)
.orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND,
"Matricula no encontrada."));
}
}