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."));
}
}
Gestión de clientes (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de clientes 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 información de los clientes, incluyendo sus datos personales y la relación con los pedidos realizados.
Funcionalidades a implementar
Crear un cliente a partir de un DTO de entrada (ClienteCrearDto), validando que el nombre, el correo electrónico y la dirección sean obligatorios y que el email tenga un formato válido.
Obtener el listado completo de clientes registrados.
Obtener un cliente a partir de su identificador.
Filtrar clientes por nombre, correo electrónico o dirección.
Actualizar completamente los datos de un cliente existente.
Actualizar parcialmente un cliente mediante un DTO (ClientePatchDto), permitiendo modificar únicamente los campos enviados en la petición.
Eliminar un cliente a partir de su identificador.
Utilizar DTOs de salida (ClienteDto) 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 cliente 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 la relación entre Cliente y Pedido mediante JPA (@OneToMany).
package com.example.spring.ventas.controller;
import com.example.spring.ventas.dto.ClienteCrearDto;
import com.example.spring.ventas.dto.ClienteDto;
import com.example.spring.ventas.dto.ClientePatchDto;
import com.example.spring.ventas.service.ClienteService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/clientes")
public class ClienteController {
// Servicio encargado de gestionar la lógica de negocio relacionada con los clientes
private final ClienteService clienteService;
// Inyección del servicio mediante constructor para mantener la clase desacoplada
public ClienteController(ClienteService clienteService) {
this.clienteService = clienteService;
}
// Obtiene la lista completa de clientes registrados
@GetMapping
public List<ClienteDto> mostrarClientes(){
return clienteService.mostrarClientes();
}
// Busca un cliente concreto mediante su identificador
@GetMapping("/{id}")
public ResponseEntity<ClienteDto> buscarClienteId(@PathVariable Long id){
return ResponseEntity.ok(clienteService.buscarPorId(id));
}
// Filtra clientes utilizando parámetros opcionales de búsqueda
@GetMapping("/filtros")
public List<ClienteDto> filtrarClientes(@RequestParam(required = false) String nombre,
@RequestParam(required = false) String email,
@RequestParam(required = false) String direccion){
return clienteService.filtrarClientes(nombre,email,direccion);
}
// Crea un nuevo cliente validando previamente los datos recibidos
@PostMapping
public ResponseEntity<ClienteDto> agregarCliente(@Valid @RequestBody ClienteCrearDto dto){
return ResponseEntity.ok(clienteService.agregarCliente(dto));
}
// Actualiza completamente los datos de un cliente existente
@PutMapping("/{id}")
public ResponseEntity<ClienteDto> actualizarCliente(@PathVariable Long id,
@Valid @RequestBody ClienteCrearDto dto){
return ResponseEntity.ok(clienteService.actualizarCliente(id,dto));
}
// Elimina un cliente mediante su identificador
@DeleteMapping("/{id}")
public ResponseEntity<Void> borrarCliente(@PathVariable Long id){
clienteService.borrarCliente(id);
return ResponseEntity.noContent().build();
}
// Actualiza parcialmente los campos enviados del cliente
@PatchMapping("/{id}")
public ResponseEntity<ClienteDto> actualizacionParcial(@PathVariable Long id,
@RequestBody ClientePatchDto dto){
return ResponseEntity.ok(clienteService.actualizacionParcial(id,dto));
}
}
package com.example.spring.ventas.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ClienteCrearDto {
// Nombre del cliente, campo obligatorio para crear un nuevo registro
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
// Correo electrónico del cliente validado con formato correcto
@NotBlank(message = "El email es obligatorio.")
@Email(message = "Formato de email no valido.")
private String email;
// Dirección del cliente, campo obligatorio para completar sus datos
@NotBlank(message = "La dirección es obligatoria.")
private String direccion;
}
package com.example.spring.ventas.dto;
import lombok.*;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ClienteDto {
// Identificador único del cliente
private Long id;
// Nombre completo del cliente
private String nombre;
// Correo electrónico asociado al cliente
private String email;
// Dirección del cliente
private String direccion;
}
package com.example.spring.ventas.mapper;
import com.example.spring.ventas.dto.ClienteCrearDto;
import com.example.spring.ventas.dto.ClienteDto;
import com.example.spring.ventas.dto.ClientePatchDto;
import com.example.spring.ventas.model.Cliente;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
@Mapper(componentModel = "spring")
public interface ClienteMapper {
// Convierte un DTO de creación en una entidad Cliente para persistir en la base de datos
Cliente toEntity(ClienteCrearDto dto);
// Convierte una entidad Cliente en un DTO para exponer los datos al cliente de la API
ClienteDto toDto(Cliente cliente);
// Actualiza parcialmente una entidad Cliente ignorando los campos que lleguen con valor null
// Permite realizar modificaciones parciales mediante operaciones PATCH
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Cliente cliente, ClientePatchDto dto);
}
package com.example.spring.ventas.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.*;
import java.util.List;
@Entity
@Table(name = "cliente")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Cliente {
// Identificador único del cliente generado automáticamente por la base de datos
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Nombre completo del cliente
private String nombre;
// Correo electrónico asociado al cliente
private String email;
// Dirección física del cliente
private String direccion;
// Relación uno a muchos: un cliente puede tener varios pedidos asociados
// mappedBy indica que la relación está gestionada por la entidad Pedido
@OneToMany(mappedBy = "cliente")
private List<Pedido> pedidos;
}
package com.example.spring.ventas.dto;
import jakarta.validation.constraints.Email;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class ClientePatchDto {
// Nombre del cliente que se desea actualizar parcialmente
private String nombre;
// Correo electrónico del cliente validado si se proporciona un nuevo valor
@Email(message = "Formato de email no valido.")
private String email;
// Dirección del cliente que se desea modificar parcialmente
private String direccion;
}
package com.example.spring.ventas.repository;
import com.example.spring.ventas.model.Cliente;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ClienteRepository extends JpaRepository<Cliente,Long> {
// Busca clientes filtrando por nombre ignorando diferencias entre mayúsculas y minúsculas
List<Cliente> findByNombreContainingIgnoreCase(String nombre);
// Busca un cliente mediante su correo electrónico exacto
List<Cliente> findByEmail(String email);
// Busca clientes cuya dirección contenga el texto indicado sin distinguir mayúsculas y minúsculas
List<Cliente> findByDireccionContainingIgnoreCase(String direccion);
}
package com.example.spring.ventas.service;
import com.example.spring.ventas.dto.ClienteCrearDto;
import com.example.spring.ventas.dto.ClienteDto;
import com.example.spring.ventas.dto.ClientePatchDto;
import com.example.spring.ventas.mapper.ClienteMapper;
import com.example.spring.ventas.model.Cliente;
import com.example.spring.ventas.repository.ClienteRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@Service
public class ClienteService {
// Repositorio encargado del acceso a datos de la entidad Cliente
private final ClienteRepository clienteRepository;
// Mapper encargado de convertir entre entidades y DTOs de Cliente
private final ClienteMapper clienteMapper;
// Inyección de dependencias mediante constructor
public ClienteService(ClienteRepository clienteRepository, ClienteMapper clienteMapper) {
this.clienteRepository = clienteRepository;
this.clienteMapper = clienteMapper;
}
// Obtiene todos los clientes y los transforma a DTOs
public List<ClienteDto> mostrarClientes(){
return transformarCliente(clienteRepository.findAll());
}
// Convierte una lista de entidades Cliente en una lista de ClienteDto
private List<ClienteDto> transformarCliente(List<Cliente> listaClientes){
return listaClientes.stream()
.map(clienteMapper::toDto)
.toList();
}
// Busca un cliente por su identificador y devuelve sus datos como DTO
public ClienteDto buscarPorId(Long id){
return clienteMapper.toDto(buscarCliente(id));
}
// Busca un cliente por ID o lanza una excepción si no existe
private Cliente buscarCliente(Long id){
Cliente cliente = clienteRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Cliente no encontrado"));
return cliente;
}
// Filtra clientes utilizando los parámetros de búsqueda disponibles
public List<ClienteDto> filtrarClientes(String nombre, String email, String direccion){
if(nombre != null && !nombre.isBlank()){
return transformarCliente(clienteRepository.findByNombreContainingIgnoreCase(nombre));
}
if(email != null && !email.isBlank()){
return transformarCliente(clienteRepository.findByEmail(email));
}
if(direccion != null && !direccion.isBlank()){
return transformarCliente(clienteRepository.findByDireccionContainingIgnoreCase(direccion));
}
return transformarCliente(clienteRepository.findAll());
}
// Crea un nuevo cliente a partir de un DTO de creación
public ClienteDto agregarCliente(ClienteCrearDto dto){
Cliente cliente = clienteMapper.toEntity(dto);
Cliente guardado = clienteRepository.save(cliente);
return clienteMapper.toDto(guardado);
}
// Actualiza completamente los datos de un cliente existente
public ClienteDto actualizarCliente(Long id, ClienteCrearDto dto){
Cliente cliente = buscarCliente(id);
cliente.setNombre(dto.getNombre());
cliente.setEmail(dto.getEmail());
cliente.setDireccion(dto.getDireccion());
return clienteMapper.toDto(clienteRepository.save(cliente));
}
// Elimina un cliente mediante su identificador
public void borrarCliente(Long id){
Cliente cliente = buscarCliente(id);
clienteRepository.delete(cliente);
}
// Actualiza parcialmente un cliente utilizando un DTO Patch y MapStruct
public ClienteDto actualizacionParcial(Long id, ClientePatchDto dto){
Cliente cliente = buscarCliente(id);
clienteMapper.update(cliente, dto);
return clienteMapper.toDto(clienteRepository.save(cliente));
}
}
Gestión de productos (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de productos 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 el catálogo de productos, incluyendo su nombre, precio y stock disponible.
Funcionalidades a implementar
Crear un producto a partir de un DTO de entrada (ProductoCrearDto), validando que el nombre, el precio y el stock sean obligatorios y tengan valores válidos.
Obtener el listado completo de productos registrados.
Obtener un producto a partir de su identificador.
Filtrar productos por nombre, precio o stock.
Actualizar completamente los datos de un producto existente.
Actualizar parcialmente un producto mediante un DTO (ProductoPatchDto), permitiendo modificar únicamente los campos enviados en la petición.
Eliminar un producto a partir de su identificador.
Utilizar DTOs de salida (ProductoDto) 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 producto 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 la relación entre Producto y LíneaPedido mediante JPA (@OneToMany).
package com.example.spring.ventas.controller;
import com.example.spring.ventas.dto.ProductoCrearDto;
import com.example.spring.ventas.dto.ProductoDto;
import com.example.spring.ventas.dto.ProductoPatchDto;
import com.example.spring.ventas.service.ProductoService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
@RestController
@RequestMapping("/productos")
public class ProductoController {
// Servicio encargado de gestionar la lógica de negocio relacionada con los productos
private final ProductoService productoService;
// Inyección del servicio mediante constructor para mantener un diseño desacoplado
public ProductoController(ProductoService productoService) {
this.productoService = productoService;
}
// Obtiene la lista completa de productos registrados
@GetMapping
public List<ProductoDto> listarProductos(){
return productoService.listarProductos();
}
// Busca un producto concreto mediante su identificador
@GetMapping("/{id}")
public ResponseEntity<ProductoDto> buscarPorId(@PathVariable Long id){
return ResponseEntity.ok(productoService.buscarPorId(id));
}
// Filtra productos mediante nombre, precio y stock utilizando parámetros opcionales
@GetMapping("/filtrar")
public ResponseEntity<List<ProductoDto>> filtrarCampos(@RequestParam(required = false) String nombre,
@RequestParam(required = false) BigDecimal precio,
@RequestParam(required = false) Integer stock){
return ResponseEntity.ok(productoService.filtrarProductos(nombre,precio,stock));
}
// Crea un nuevo producto validando previamente los datos recibidos
@PostMapping
public ResponseEntity<ProductoDto> agregarProducto(@Valid @RequestBody ProductoCrearDto dto){
return ResponseEntity.ok(productoService.agregarProducto(dto));
}
// Actualiza completamente los datos de un producto existente
@PutMapping("/{id}")
public ResponseEntity<ProductoDto> actualizarProducto(@PathVariable Long id,
@Valid @RequestBody ProductoCrearDto dto){
return ResponseEntity.ok(productoService.actualizarProducto(id,dto));
}
// Actualiza parcialmente los campos enviados del producto
@PatchMapping("/{id}")
public ResponseEntity<ProductoDto> actualizacionParcial(@PathVariable Long id,
@RequestBody ProductoPatchDto dto){
return ResponseEntity.ok(productoService.actualizacionParcial(id,dto));
}
// Elimina un producto mediante su identificador
@DeleteMapping("/{id}")
public ResponseEntity<Void> borrarProducto(@PathVariable Long id){
productoService.borrarProducto(id);
return ResponseEntity.noContent().build();
}
}
package com.example.spring.ventas.dto;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import java.math.BigDecimal;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProductoCrearDto {
// Nombre descriptivo del producto, obligatorio para crear un registro
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
// Precio del producto validado para evitar valores nulos o negativos
@NotNull(message = "El precio es obligatorio.")
@DecimalMin(value = "0.01", message = "El precio tiene que ser mayor que 0.")
private BigDecimal precio;
// Cantidad disponible del producto en inventario
// Se valida que exista y tenga al menos una unidad disponible
@NotNull(message = "El stock es obligatorio.")
@Min(value = 1, message = "El stock tiene que ser mayor que 0.")
private Integer stock;
}
package com.example.spring.ventas.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProductoDto {
// Identificador único del producto
private Long id;
// Nombre descriptivo del producto
private String nombre;
// Precio actual del producto
private BigDecimal precio;
// Número de unidades disponibles en inventario
private Integer stock;
}
package com.example.spring.ventas.mapper;
import com.example.spring.ventas.dto.ProductoCrearDto;
import com.example.spring.ventas.dto.ProductoDto;
import com.example.spring.ventas.dto.ProductoPatchDto;
import com.example.spring.ventas.model.Producto;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
@Mapper(componentModel = "spring")
public interface ProductoMapper {
// Convierte un DTO de creación en una entidad Producto para guardarla en la base de datos
Producto toEntity(ProductoCrearDto dto);
// Convierte una entidad Producto en un DTO para devolver la información mediante la API REST
ProductoDto toDto(Producto producto);
// Actualiza parcialmente una entidad Producto ignorando los campos que reciban un valor null
// Permite modificar únicamente los atributos enviados mediante una petición PATCH
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Producto producto, ProductoPatchDto dto);
}
package com.example.spring.ventas.model;
import jakarta.persistence.*;
import lombok.*;
import java.math.BigDecimal;
import java.util.List;
@Entity
@Table(name = "producto")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Producto {
// Identificador único del producto generado automáticamente por la base de datos
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Nombre descriptivo del producto
private String nombre;
// Precio actual del producto
private BigDecimal precio;
// Número de unidades disponibles en inventario
private Integer stock;
// Relación uno a muchos: un producto puede aparecer en varias líneas de pedido
// mappedBy indica que la relación está gestionada desde la entidad LineaPedido
@OneToMany(mappedBy = "producto")
private List<LineaPedido> lineaPedido;
}
package com.example.spring.ventas.dto;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import lombok.*;
import java.math.BigDecimal;
@Getter
@Setter
@NoArgsConstructor
public class ProductoPatchDto {
// Nombre del producto que se desea modificar parcialmente
private String nombre;
// Precio del producto que se desea actualizar parcialmente
// Se valida que, si se informa, tenga un valor positivo
@DecimalMin(value = "0.01", message = "El precio tiene que ser mayor que 0.")
private BigDecimal precio;
// Cantidad disponible del producto que se desea actualizar parcialmente
// Se valida que, si se informa, tenga al menos una unidad
@Min(value = 1, message = "El stock tiene que ser mayor que 0.")
private Integer stock;
}
package com.example.spring.ventas.repository;
import com.example.spring.ventas.model.Producto;
import org.springframework.data.jpa.repository.JpaRepository;
import java.math.BigDecimal;
import java.util.List;
public interface ProductoRepository extends JpaRepository<Producto,Long> {
// Busca productos cuyo nombre contenga el texto indicado ignorando mayúsculas y minúsculas
List<Producto> findByNombreContainingIgnoreCase(String nombre);
// Busca productos filtrando por un precio concreto
List<Producto> findByPrecio(BigDecimal precio);
// Busca productos filtrando por la cantidad disponible en stock
List<Producto> findByStock(Integer stock);
}
package com.example.spring.ventas.service;
import com.example.spring.ventas.dto.ProductoCrearDto;
import com.example.spring.ventas.dto.ProductoDto;
import com.example.spring.ventas.dto.ProductoPatchDto;
import com.example.spring.ventas.mapper.ProductoMapper;
import com.example.spring.ventas.model.Producto;
import com.example.spring.ventas.repository.ProductoRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.util.List;
@Service
public class ProductoService {
// Repositorio encargado del acceso a datos de los productos
private final ProductoRepository productoRepository;
// Mapper encargado de convertir entre entidades y DTOs de producto
private final ProductoMapper productoMapper;
// Inyección de dependencias mediante constructor
public ProductoService(ProductoRepository productoRepository, ProductoMapper productoMapper) {
this.productoRepository = productoRepository;
this.productoMapper = productoMapper;
}
// Convierte una lista de entidades Producto en una lista de DTOs
private List<ProductoDto> transformarProductos(List<Producto> listaProductos){
return listaProductos.stream()
.map(productoMapper::toDto)
.toList();
}
// Busca un producto por su identificador o lanza una excepción si no existe
private Producto buscarProducto(Long id){
return productoRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Producto no encontrado"));
}
// Obtiene todos los productos registrados
public List<ProductoDto> listarProductos(){
return transformarProductos(productoRepository.findAll());
}
// Busca un producto por ID y devuelve sus datos mediante un DTO
public ProductoDto buscarPorId(Long id){
return productoMapper.toDto(buscarProducto(id));
}
// Filtra productos mediante nombre, precio o stock
public List<ProductoDto> filtrarProductos(String nombre, BigDecimal precio, Integer stock){
if(nombre != null && !nombre.isBlank()){
return transformarProductos(productoRepository.findByNombreContainingIgnoreCase(nombre));
}
if(precio != null){
return transformarProductos(productoRepository.findByPrecio(precio));
}
if(stock != null){
return transformarProductos(productoRepository.findByStock(stock));
}
return transformarProductos(productoRepository.findAll());
}
// Crea un nuevo producto a partir de un DTO de creación
public ProductoDto agregarProducto(ProductoCrearDto dto){
Producto producto = productoMapper.toEntity(dto);
Producto guardado = productoRepository.save(producto);
return productoMapper.toDto(guardado);
}
// Actualiza completamente los datos de un producto existente
public ProductoDto actualizarProducto(Long id, ProductoCrearDto dto){
Producto producto = buscarProducto(id);
producto.setNombre(dto.getNombre());
producto.setPrecio(dto.getPrecio());
producto.setStock(dto.getStock());
return productoMapper.toDto(productoRepository.save(producto));
}
// Actualiza parcialmente un producto utilizando un DTO Patch y MapStruct
public ProductoDto actualizacionParcial(Long id, ProductoPatchDto dto){
Producto producto = buscarProducto(id);
productoMapper.update(producto, dto);
return productoMapper.toDto(productoRepository.save(producto));
}
// Elimina un producto mediante su identificador
public void borrarProducto(Long id){
Producto producto = buscarProducto(id);
productoRepository.delete(producto);
}
}
Gestión de pedidos (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de pedidos 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 los pedidos realizados por los clientes, registrando la fecha del pedido, su estado y el cliente asociado.
Funcionalidades a implementar
Crear un pedido a partir de un DTO de entrada (PedidoCrearDto), validando que la fecha, el estado y el cliente sean obligatorios y que la fecha no sea posterior a la actual.
Asociar cada pedido a un cliente existente.
Obtener el listado completo de pedidos registrados.
Obtener un pedido a partir de su identificador.
Filtrar pedidos por fecha o estado.
Actualizar completamente los datos de un pedido existente.
Actualizar parcialmente un pedido mediante un DTO (PedidoPatchDto), permitiendo modificar únicamente los campos enviados en la petición.
Eliminar un pedido a partir de su identificador.
Utilizar DTOs de salida (PedidoDto) 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 pedido 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 la relación entre Cliente y Pedido mediante JPA (@ManyToOne).
Uso de un tipo enumerado (EstadoEnum) para representar el estado del pedido.
package com.example.spring.ventas.controller;
import com.example.spring.ventas.dto.PedidoCrearDto;
import com.example.spring.ventas.dto.PedidoDto;
import com.example.spring.ventas.dto.PedidoPatchDto;
import com.example.spring.ventas.enums.EstadoEnum;
import com.example.spring.ventas.service.PedidoService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/pedidos")
public class PedidoController {
// Servicio encargado de gestionar la lógica de negocio relacionada con los pedidos
private final PedidoService pedidoService;
// Inyección del servicio mediante constructor para mantener un diseño desacoplado
public PedidoController(PedidoService pedidoService) {
this.pedidoService = pedidoService;
}
// Obtiene la lista completa de pedidos registrados
@GetMapping
public ResponseEntity<List<PedidoDto>> listarProductos(){
return ResponseEntity.ok(pedidoService.listarPedidos());
}
// Busca un pedido concreto mediante su identificador
@GetMapping("/{id}")
public ResponseEntity<PedidoDto> buscarPorId(@PathVariable Long id){
return ResponseEntity.ok(pedidoService.buscarPorId(id));
}
// Filtra pedidos mediante fecha y estado utilizando parámetros opcionales
@GetMapping("/filtros")
public ResponseEntity<List<PedidoDto>> filtrarCampos(@RequestParam(required = false) LocalDate fecha,
@RequestParam(required = false) EstadoEnum estado){
return ResponseEntity.ok(pedidoService.filtrarCampos(fecha,estado));
}
// Crea un nuevo pedido validando previamente los datos recibidos
@PostMapping
public ResponseEntity<PedidoDto> agregarPedido(@Valid @RequestBody PedidoCrearDto dto){
return ResponseEntity.ok(pedidoService.agregarPedido(dto));
}
// Actualiza completamente los datos de un pedido existente
@PutMapping("/{id}")
public ResponseEntity<PedidoDto> actualizarPedido(@PathVariable Long id,
@Valid @RequestBody PedidoCrearDto dto){
return ResponseEntity.ok(pedidoService.actualizarPedido(id,dto));
}
// Actualiza parcialmente los campos enviados del pedido
@PatchMapping("/{id}")
public ResponseEntity<PedidoDto> actualizacionParcial(@PathVariable Long id,
@RequestBody PedidoPatchDto dto){
return ResponseEntity.ok(pedidoService.actualizacionParcial(id,dto));
}
// Elimina un pedido mediante su identificador
@DeleteMapping("/{id}")
public ResponseEntity<Void> borrarPedido(@PathVariable Long id){
pedidoService.borrarPedido(id);
return ResponseEntity.noContent().build();
}
}
package com.example.spring.ventas.dto;
import com.example.spring.ventas.enums.EstadoEnum;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import lombok.*;
import java.time.LocalDate;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PedidoCrearDto {
// Fecha en la que se realiza el pedido
// Se valida que sea obligatoria y no pertenezca a una fecha futura
@NotNull(message = "La fecha es obligatoria")
@PastOrPresent(message = "La fecha no puede ser mayor que la fecha actual.")
private LocalDate fecha;
// Estado actual del pedido dentro del flujo de gestión
@NotNull(message = "El estado es obligatorio.")
private EstadoEnum estado;
// Identificador del cliente asociado al pedido
@NotNull(message = "El cliente es obligatorio.")
private Long clienteId;
}
package com.example.spring.ventas.dto;
import com.example.spring.ventas.enums.EstadoEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PedidoDto {
// Identificador único del pedido
private Long id;
// Fecha en la que se realizó el pedido
private LocalDate fecha;
// Estado actual del pedido dentro del proceso de gestión
private EstadoEnum estado;
// Identificador del cliente asociado al pedido
private Long clienteId;
}
package com.example.spring.ventas.mapper;
import com.example.spring.ventas.dto.PedidoCrearDto;
import com.example.spring.ventas.dto.PedidoDto;
import com.example.spring.ventas.dto.PedidoPatchDto;
import com.example.spring.ventas.model.Pedido;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
@Mapper(componentModel = "spring")
public interface PedidoMapper {
// Convierte un DTO de creación en una entidad Pedido para su persistencia en la base de datos
Pedido toEntity(PedidoCrearDto dto);
// Convierte una entidad Pedido en un DTO para exponer la información mediante la API REST
PedidoDto toDto(Pedido pedido);
// Actualiza parcialmente una entidad Pedido ignorando los campos que reciban un valor null
// Permite realizar modificaciones parciales mediante operaciones PATCH
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Pedido pedido, PedidoPatchDto dto);
}
package com.example.spring.ventas.model;
import com.example.spring.ventas.enums.EstadoEnum;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDate;
import java.util.List;
@Entity
@Table(name = "pedido")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Pedido {
// Identificador único del pedido generado automáticamente por la base de datos
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Fecha en la que se realiza el pedido
private LocalDate fecha;
// Estado actual del pedido mediante un enumerado de la aplicación
private EstadoEnum estado;
// Relación muchos a uno: varios pedidos pueden pertenecer al mismo cliente
@ManyToOne
@JoinColumn(name = "cliente_id")
private Cliente cliente;
// Relación uno a muchos: un pedido está compuesto por varias líneas de pedido
// mappedBy indica que la relación está gestionada desde la entidad LineaPedido
@OneToMany(mappedBy = "pedido")
private List<LineaPedido> lineaPedido;
}
package com.example.spring.ventas.dto;
import com.example.spring.ventas.enums.EstadoEnum;
import jakarta.validation.constraints.PastOrPresent;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDate;
@Getter
@Setter
@NoArgsConstructor
public class PedidoPatchDto {
// Fecha del pedido que se desea actualizar parcialmente
// Se valida que, si se informa, no sea una fecha futura
@PastOrPresent(message = "La fecha no puede ser mayor que la fecha actual.")
private LocalDate fecha;
// Estado del pedido que se desea modificar parcialmente
private EstadoEnum estado;
}
package com.example.spring.ventas.repository;
import com.example.spring.ventas.enums.EstadoEnum;
import com.example.spring.ventas.model.Pedido;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
public interface PedidoRepository extends JpaRepository<Pedido,Long> {
// Busca pedidos realizados en una fecha concreta
List<Pedido> findByFecha(LocalDate fecha);
// Busca pedidos filtrando por su estado actual
List<Pedido> findByEstado(EstadoEnum estado);
}
package com.example.spring.ventas.service;
import com.example.spring.ventas.dto.PedidoCrearDto;
import com.example.spring.ventas.dto.PedidoDto;
import com.example.spring.ventas.dto.PedidoPatchDto;
import com.example.spring.ventas.enums.EstadoEnum;
import com.example.spring.ventas.mapper.PedidoMapper;
import com.example.spring.ventas.model.Cliente;
import com.example.spring.ventas.model.Pedido;
import com.example.spring.ventas.repository.ClienteRepository;
import com.example.spring.ventas.repository.PedidoRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.util.List;
@Service
public class PedidoService {
// Repositorio encargado del acceso a datos de los pedidos
private final PedidoRepository pedidoRepository;
// Mapper encargado de convertir entre entidades y DTOs de pedido
private final PedidoMapper pedidoMapper;
// Repositorio utilizado para obtener los clientes relacionados con los pedidos
private final ClienteRepository clienteRepository;
// Inyección de dependencias mediante constructor
public PedidoService(PedidoRepository pedidoRepository,
PedidoMapper pedidoMapper,
ClienteRepository clienteRepository) {
this.pedidoRepository = pedidoRepository;
this.pedidoMapper = pedidoMapper;
this.clienteRepository = clienteRepository;
}
// Convierte una lista de entidades Pedido en una lista de DTOs
private List<PedidoDto> transformarPedidos(List<Pedido> listaPedidos){
return listaPedidos.stream()
.map(pedidoMapper::toDto)
.toList();
}
// Busca un pedido por su identificador o lanza una excepción si no existe
private Pedido buscarPedido(Long id){
return pedidoRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Pedido no encontrado"));
}
// Obtiene todos los pedidos registrados
public List<PedidoDto> listarPedidos(){
return transformarPedidos(pedidoRepository.findAll());
}
// Busca un pedido por ID y devuelve sus datos mediante un DTO
public PedidoDto buscarPorId(Long id){
return pedidoMapper.toDto(buscarPedido(id));
}
// Filtra pedidos utilizando fecha o estado como criterios de búsqueda
public List<PedidoDto> filtrarCampos(LocalDate fecha, EstadoEnum estado){
if(fecha != null){
return transformarPedidos(pedidoRepository.findByFecha(fecha));
}
if(estado != null){
return transformarPedidos(pedidoRepository.findByEstado(estado));
}
return transformarPedidos(pedidoRepository.findAll());
}
// Crea un nuevo pedido asociándolo a un cliente existente
public PedidoDto agregarPedido(PedidoCrearDto dto){
Cliente cliente = clienteRepository.findById(dto.getClienteId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Cliente no encontrado"));
Pedido pedido = pedidoMapper.toEntity(dto);
pedido.setCliente(cliente);
Pedido guardado = pedidoRepository.save(pedido);
return pedidoMapper.toDto(guardado);
}
// Actualiza completamente los datos de un pedido existente
public PedidoDto actualizarPedido(Long id, PedidoCrearDto dto){
Cliente cliente = clienteRepository.findById(dto.getClienteId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Cliente no encontrado"));
Pedido pedido = buscarPedido(id);
pedido.setFecha(dto.getFecha());
pedido.setEstado(dto.getEstado());
pedido.setCliente(cliente);
return pedidoMapper.toDto(pedidoRepository.save(pedido));
}
// Actualiza parcialmente los datos de un pedido mediante un DTO Patch
public PedidoDto actualizacionParcial(Long id, PedidoPatchDto dto){
Pedido pedido = buscarPedido(id);
pedidoMapper.update(pedido, dto);
return pedidoMapper.toDto(pedidoRepository.save(pedido));
}
// Elimina un pedido mediante su identificador
public void borrarPedido(Long id){
Pedido pedido = buscarPedido(id);
pedidoRepository.delete(pedido);
}
}
Gestión de líneas de pedido (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de líneas de pedido 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 los productos incluidos en cada pedido, indicando la cantidad solicitada y el precio unitario de cada línea.
Funcionalidades a implementar
Crear una línea de pedido a partir de un DTO de entrada (LineaPedidoCrearDto), validando que la cantidad, el precio unitario, el pedido y el producto sean obligatorios y tengan valores válidos.
Asociar cada línea de pedido a un pedido y a un producto existentes.
Obtener el listado completo de líneas de pedido registradas.
Obtener una línea de pedido a partir de su identificador.
Filtrar líneas de pedido por cantidad o precio unitario.
Actualizar completamente los datos de una línea de pedido existente.
Actualizar parcialmente una línea de pedido mediante un DTO (LineaPedidoPatchDto), permitiendo modificar únicamente los campos enviados en la petición.
Eliminar una línea de pedido a partir de su identificador.
Utilizar DTOs de salida (LineaPedidoDto) 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 línea de pedido 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 las relaciones entre Pedido, Producto y LineaPedido mediante JPA (@ManyToOne).
Asociación de cada línea de pedido a un pedido y un producto existentes antes de su persistencia.
package com.example.spring.ventas.controller;
import com.example.spring.ventas.dto.LineaPedidoCrearDto;
import com.example.spring.ventas.dto.LineaPedidoDto;
import com.example.spring.ventas.dto.LineaPedidoPatchDto;
import com.example.spring.ventas.service.LineaPedidoService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
@RestController
@RequestMapping("/lineas")
public class LineaPedidoController {
// Servicio encargado de gestionar la lógica de negocio de las líneas de pedido
private final LineaPedidoService lineaPedidoService;
// Inyección del servicio mediante constructor para facilitar el mantenimiento y las pruebas
public LineaPedidoController(LineaPedidoService lineaPedidoService) {
this.lineaPedidoService = lineaPedidoService;
}
// Obtiene todas las líneas de pedido registradas
@GetMapping
public ResponseEntity<List<LineaPedidoDto>> listarLineas(){
return ResponseEntity.ok(lineaPedidoService.listarLineas());
}
// Busca una línea de pedido mediante su identificador
@GetMapping("/{id}")
public ResponseEntity<LineaPedidoDto> buscarPorId(@PathVariable Long id){
return ResponseEntity.ok(lineaPedidoService.buscarPorId(id));
}
// Filtra líneas de pedido mediante criterios opcionales de cantidad y precio
@GetMapping("/filtrar")
public ResponseEntity<List<LineaPedidoDto>> filtrarLineas(@RequestParam(required = false) Integer cantidad,
@RequestParam(required = false) BigDecimal precio){
return ResponseEntity.ok(lineaPedidoService.filtrarLineas(cantidad,precio));
}
// Crea una nueva línea de pedido validando los datos recibidos
@PostMapping
public ResponseEntity<LineaPedidoDto> agregarLinea(@Valid @RequestBody LineaPedidoCrearDto dto){
return ResponseEntity.ok(lineaPedidoService.agregarLinea(dto));
}
// Actualiza completamente una línea de pedido existente
@PutMapping("/{id}")
public ResponseEntity<LineaPedidoDto> actualizarLinea(@PathVariable Long id,
@Valid @RequestBody LineaPedidoCrearDto dto){
return ResponseEntity.ok(lineaPedidoService.actualizarLinea(id,dto));
}
// Actualiza parcialmente los datos de una línea de pedido
@PatchMapping("/{id}")
public ResponseEntity<LineaPedidoDto> actualizacionParcial(@PathVariable Long id,
@RequestBody LineaPedidoPatchDto dto){
return ResponseEntity.ok(lineaPedidoService.actualizacionParcial(id,dto));
}
// Elimina una línea de pedido mediante su identificador
@DeleteMapping("/{id}")
public ResponseEntity<Void> borrarLinea(@PathVariable Long id){
lineaPedidoService.borrarLinea(id);
return ResponseEntity.noContent().build();
}
}
package com.example.spring.ventas.dto;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import java.math.BigDecimal;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LineaPedidoCrearDto {
// Cantidad de unidades del producto incluida en la línea de pedido
// Validada para evitar valores nulos o cantidades menores que una unidad
@NotNull(message = "La cantidad es obligatoria.")
@Min(value = 1, message = "La cantidad no puede ser inferior a 1.")
private Integer cantidad;
// Precio del producto aplicado en el momento de crear la línea de pedido
// Se valida que exista y que tenga un valor positivo
@NotNull(message = "El precio es obligatorio.")
@DecimalMin(value = "0.01", message = "El precio no puede ser inferior a 1 céntimo.")
private BigDecimal precioUnitario;
// Identificador del pedido al que pertenece la línea
@NotNull(message = "El pedido es obligatorio.")
private Long pedidoId;
// Identificador del producto asociado a la línea de pedido
@NotNull(message = "El producto es obligatorio.")
private Long productoId;
}
package com.example.spring.ventas.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LineaPedidoDto {
// Identificador único de la línea de pedido
private Long id;
// Cantidad de unidades del producto asociadas a esta línea
private Integer cantidad;
// Precio aplicado al producto en el momento de realizar el pedido
private BigDecimal precioUnitario;
// Identificador del pedido al que pertenece la línea
private Long pedidoId;
// Identificador del producto asociado a la línea de pedido
private Long productoId;
}
package com.example.spring.ventas.mapper;
import com.example.spring.ventas.dto.LineaPedidoCrearDto;
import com.example.spring.ventas.dto.LineaPedidoDto;
import com.example.spring.ventas.dto.LineaPedidoPatchDto;
import com.example.spring.ventas.model.LineaPedido;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
@Mapper(componentModel = "spring")
public interface LineaPedidoMapper {
// Convierte un DTO de creación en una entidad LineaPedido para su almacenamiento en la base de datos
LineaPedido toEntity(LineaPedidoCrearDto dto);
// Convierte una entidad LineaPedido en un DTO para devolver los datos mediante la API REST
LineaPedidoDto toDto(LineaPedido lineaPedido);
// Actualiza parcialmente una entidad LineaPedido ignorando los atributos recibidos con valor null
// Facilita la implementación de actualizaciones parciales mediante PATCH
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget LineaPedido lineaPedido, LineaPedidoPatchDto dto);
}
package com.example.spring.ventas.model;
import jakarta.persistence.*;
import lombok.*;
import java.math.BigDecimal;
@Entity
@Table(name = "linea_pedido")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LineaPedido {
// Identificador único de la línea de pedido generado automáticamente
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Cantidad de unidades del producto incluidas en la línea de pedido
private Integer cantidad;
// Precio del producto en el momento de realizar el pedido
private BigDecimal precioUnitario;
// Relación muchos a uno: varias líneas de pedido pertenecen a un mismo pedido
@ManyToOne
@JoinColumn(name = "pedido_id")
private Pedido pedido;
// Relación muchos a uno: varias líneas de pedido pueden hacer referencia al mismo producto
@ManyToOne
@JoinColumn(name = "producto_id")
private Producto producto;
}
package com.example.spring.ventas.dto;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
@NoArgsConstructor
public class LineaPedidoPatchDto {
// Cantidad de unidades del producto que se desea modificar parcialmente
// Se valida que, si se informa, tenga un valor mínimo de una unidad
@Min(value = 1, message = "La cantidad no puede ser inferior a 1.")
private Integer cantidad;
// Precio del producto que se desea actualizar parcialmente
// Se valida que, si se informa, tenga un valor positivo
@DecimalMin(value = "0.01", message = "El precio no puede ser inferior a 1 centimo.")
private BigDecimal precioUnitario;
}
package com.example.spring.ventas.repository;
import com.example.spring.ventas.model.LineaPedido;
import org.springframework.data.jpa.repository.JpaRepository;
import java.math.BigDecimal;
import java.util.List;
public interface LineaPedidoRepository extends JpaRepository<LineaPedido,Long> {
// Busca líneas de pedido filtrando por la cantidad exacta de unidades
List<LineaPedido> findByCantidad(Integer cantidad);
// Busca líneas de pedido filtrando por el precio unitario exacto del producto
List<LineaPedido> findByPrecioUnitario(BigDecimal precio);
}
package com.example.spring.ventas.service;
import com.example.spring.ventas.dto.LineaPedidoCrearDto;
import com.example.spring.ventas.dto.LineaPedidoDto;
import com.example.spring.ventas.dto.LineaPedidoPatchDto;
import com.example.spring.ventas.mapper.LineaPedidoMapper;
import com.example.spring.ventas.model.LineaPedido;
import com.example.spring.ventas.model.Pedido;
import com.example.spring.ventas.model.Producto;
import com.example.spring.ventas.repository.LineaPedidoRepository;
import com.example.spring.ventas.repository.PedidoRepository;
import com.example.spring.ventas.repository.ProductoRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.util.List;
@Service
public class LineaPedidoService {
// Repositorio encargado del acceso a datos de las líneas de pedido
private final LineaPedidoRepository lineaPedidoRepository;
// Mapper encargado de convertir entre entidades y DTOs de línea de pedido
private final LineaPedidoMapper lineaPedidoMapper;
// Repositorio utilizado para obtener los pedidos relacionados
private final PedidoRepository pedidoRepository;
// Repositorio utilizado para obtener los productos relacionados
private final ProductoRepository productoRepository;
// Inyección de dependencias mediante constructor
public LineaPedidoService(LineaPedidoRepository lineaPedidoRepository,
LineaPedidoMapper lineaPedidoMapper,
PedidoRepository pedidoRepository,
ProductoRepository productoRepository) {
this.lineaPedidoRepository = lineaPedidoRepository;
this.lineaPedidoMapper = lineaPedidoMapper;
this.pedidoRepository = pedidoRepository;
this.productoRepository = productoRepository;
}
// Convierte una lista de entidades LineaPedido en una lista de DTOs
private List<LineaPedidoDto> transformarLineas(List<LineaPedido> listaLineas){
return listaLineas.stream()
.map(lineaPedidoMapper::toDto)
.toList();
}
// Busca una línea de pedido por su identificador o lanza una excepción si no existe
private LineaPedido buscarLinea(Long id){
return lineaPedidoRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Línea no encontrada"));
}
// Obtiene todas las líneas de pedido registradas
public List<LineaPedidoDto> listarLineas(){
return transformarLineas(lineaPedidoRepository.findAll());
}
// Busca una línea de pedido por ID y devuelve sus datos como DTO
public LineaPedidoDto buscarPorId(Long id){
return lineaPedidoMapper.toDto(buscarLinea(id));
}
// Filtra líneas de pedido por cantidad o precio unitario
public List<LineaPedidoDto> filtrarLineas(Integer cantidad, BigDecimal precio){
if(cantidad != null){
return transformarLineas(lineaPedidoRepository.findByCantidad(cantidad));
}
if(precio != null){
return transformarLineas(lineaPedidoRepository.findByPrecioUnitario(precio));
}
return transformarLineas(lineaPedidoRepository.findAll());
}
// Crea una nueva línea de pedido asociando un pedido y un producto existentes
public LineaPedidoDto agregarLinea(LineaPedidoCrearDto dto){
Pedido pedido = pedidoRepository.findById(dto.getPedidoId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Pedido no encontrado"));
Producto producto = productoRepository.findById(dto.getProductoId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Producto no encontrado"));
LineaPedido lineaPedido = lineaPedidoMapper.toEntity(dto);
lineaPedido.setPedido(pedido);
lineaPedido.setProducto(producto);
LineaPedido guardado = lineaPedidoRepository.save(lineaPedido);
return lineaPedidoMapper.toDto(guardado);
}
// Actualiza completamente una línea de pedido existente
public LineaPedidoDto actualizarLinea(Long id, LineaPedidoCrearDto dto){
Pedido pedido = pedidoRepository.findById(dto.getPedidoId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Pedido no encontrado"));
Producto producto = productoRepository.findById(dto.getProductoId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Producto no encontrado"));
LineaPedido lineaPedido = buscarLinea(id);
lineaPedido.setCantidad(dto.getCantidad());
lineaPedido.setPrecioUnitario(dto.getPrecioUnitario());
lineaPedido.setPedido(pedido);
lineaPedido.setProducto(producto);
LineaPedido guardado = lineaPedidoRepository.save(lineaPedido);
return lineaPedidoMapper.toDto(guardado);
}
// Actualiza parcialmente una línea de pedido mediante un DTO Patch
public LineaPedidoDto actualizacionParcial(Long id, LineaPedidoPatchDto dto){
LineaPedido lineaPedido = buscarLinea(id);
lineaPedidoMapper.update(lineaPedido, dto);
return lineaPedidoMapper.toDto(lineaPedidoRepository.save(lineaPedido));
}
// Elimina una línea de pedido mediante su identificador
public void borrarLinea(Long id){
LineaPedido lineaPedido = buscarLinea(id);
lineaPedidoRepository.delete(lineaPedido);
}
}
Gestión de autores (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de autores 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 información de los autores de una biblioteca, incluyendo sus datos personales y la relación con los libros escritos.
Funcionalidades a implementar
Crear un autor a partir de un DTO de entrada (AutorCrearDto), validando que el nombre, la nacionalidad y la fecha de nacimiento sean obligatorios, y que la fecha de nacimiento no sea futura.
Obtener el listado completo de autores registrados.
Obtener un autor a partir de su identificador.
Filtrar autores por nombre o nacionalidad.
Actualizar completamente los datos de un autor existente.
Actualizar parcialmente un autor mediante un DTO (AutorPatchDto), permitiendo modificar únicamente los campos enviados en la petición.
Eliminar un autor a partir de su identificador.
Utilizar DTOs de salida (AutorDto) 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 autor que no exista.
Gestionar la relación entre Autor y Libro mediante JPA (@OneToMany).
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.
Implementación de mappers mediante MapStruct.
Gestión de relaciones entre entidades mediante JPA.
Separación de responsabilidades entre Controller, Service y Repository.
package es.pulsoft.spring_biblioteca.controller;
import es.pulsoft.spring_biblioteca.dto.AutorCrearDto;
import es.pulsoft.spring_biblioteca.dto.AutorDto;
import es.pulsoft.spring_biblioteca.dto.AutorPatchDto;
import es.pulsoft.spring_biblioteca.service.AutorService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
/**
* Controlador REST encargado de gestionar las operaciones
* relacionadas con los autores.
*/
@RestController
@RequestMapping("/autores")
public class AutorController {
// Servicio que contiene la lógica de negocio de los autores.
private final AutorService autorService;
public AutorController(AutorService autorService) {
this.autorService = autorService;
}
// Obtiene el listado completo de autores.
@GetMapping
public ResponseEntity<List<AutorDto>> listarAutores() {
return ResponseEntity.ok(autorService.listarAutores());
}
// Obtiene un autor por su identificador.
@GetMapping("/{id}")
public ResponseEntity<AutorDto> buscarPorId(@PathVariable Long id) {
return ResponseEntity.ok(autorService.buscarPorId(id));
}
// Filtra autores por nombre, nacionalidad o fecha de nacimiento.
@GetMapping("/filtrar")
public ResponseEntity<List<AutorDto>> filtrarAutores(
@RequestParam(required = false) String nombre,
@RequestParam(required = false) String nacionalidad,
@RequestParam(required = false) LocalDate fechaNacimiento) {
return ResponseEntity.ok(
autorService.filtrarAutores(nombre, nacionalidad, fechaNacimiento));
}
// Crea un nuevo autor.
@PostMapping
public ResponseEntity<AutorDto> agregarAutor(@Valid @RequestBody AutorCrearDto dto) {
return ResponseEntity.ok(autorService.agregarAutor(dto));
}
// Actualiza completamente un autor existente.
@PutMapping("/{id}")
public ResponseEntity<AutorDto> actualizarAutor(
@PathVariable Long id,
@Valid @RequestBody AutorCrearDto dto) {
return ResponseEntity.ok(autorService.actualizarAutor(id, dto));
}
// Actualiza parcialmente un autor existente.
@PatchMapping("/{id}")
public ResponseEntity<AutorDto> actualizacionParcial(
@PathVariable Long id,
@Valid @RequestBody AutorPatchDto dto) {
return ResponseEntity.ok(autorService.actualizacionParcial(id, dto));
}
// Elimina un autor por su identificador.
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminarAutor(@PathVariable Long id) {
autorService.eliminarAutor(id);
return ResponseEntity.noContent().build();
}
}
package es.pulsoft.spring_biblioteca.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import lombok.*;
import java.time.LocalDate;
/**
* DTO utilizado para crear un nuevo autor.
* Contiene únicamente los datos necesarios para el alta.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AutorCrearDto {
// Nombre completo del autor.
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
// Nacionalidad del autor.
@NotBlank(message = "La nacionalidad es obligatoria.")
private String nacionalidad;
// Fecha de nacimiento del autor.
// No puede ser una fecha futura.
@NotNull(message = "La fecha de nacimiento es obligatoria.")
@PastOrPresent(message = "La fecha no puede ser futura.")
private LocalDate fechaNacimiento;
}
package es.pulsoft.spring_biblioteca.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 un autor.
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AutorDto {
// Identificador único del autor.
private Long id;
// Nombre completo del autor.
private String nombre;
// Nacionalidad del autor.
private String nacionalidad;
// Fecha de nacimiento del autor.
private LocalDate fechaNacimiento;
}
package es.pulsoft.spring_biblioteca.mapper;
import es.pulsoft.spring_biblioteca.dto.AutorCrearDto;
import es.pulsoft.spring_biblioteca.dto.AutorDto;
import es.pulsoft.spring_biblioteca.dto.AutorPatchDto;
import es.pulsoft.spring_biblioteca.model.Autor;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
/**
* Mapper encargado de convertir entre la entidad Autor y sus distintos DTOs.
*/
@Mapper(componentModel = "spring")
public interface AutorMapper {
/**
* Convierte un DTO de creación en una entidad Autor.
*/
Autor toEntity(AutorCrearDto dto);
/**
* Convierte una entidad Autor en un DTO de respuesta.
*/
AutorDto toDto(Autor autor);
/**
* Actualiza parcialmente una entidad Autor con los datos recibidos
* en el DTO, ignorando los campos con valor null.
*/
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Autor autor, AutorPatchDto dto);
}
package es.pulsoft.spring_biblioteca.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDate;
import java.util.List;
/**
* Entidad que representa un autor dentro del sistema de biblioteca.
* Un autor puede tener asociados varios libros.
*/
@Entity
@Table(name = "autor")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Autor {
// Identificador único del autor generado automáticamente.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Nombre completo del autor.
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
// País de origen del autor.
@NotBlank(message = "La nacionalidad es obligatoria.")
private String nacionalidad;
// Fecha de nacimiento del autor. No puede ser futura.
@NotNull(message = "La fecha de nacimiento es obligatoria.")
@PastOrPresent(message = "La fecha no puede ser futura.")
private LocalDate fechaNacimiento;
// Relación uno a muchos: un autor puede tener varios libros.
// La relación se gestiona desde la entidad Libro mediante el campo autor.
@OneToMany(mappedBy = "autor")
private List<Libro> libros;
}
package es.pulsoft.spring_biblioteca.dto;
import jakarta.validation.constraints.PastOrPresent;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDate;
/**
* DTO utilizado para la actualización parcial de un autor.
* Todos los campos son opcionales.
*/
@Getter
@Setter
@NoArgsConstructor
public class AutorPatchDto {
// Nombre del autor.
private String nombre;
// Nacionalidad del autor.
private String nacionalidad;
// Fecha de nacimiento del autor.
// Si se informa, no puede ser una fecha futura.
@PastOrPresent(message = "La fecha no puede ser futura.")
private LocalDate fechaNacimiento;
}
package es.pulsoft.spring_biblioteca.repository;
import es.pulsoft.spring_biblioteca.model.Autor;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
/**
* Repositorio encargado de gestionar el acceso a los datos
* de la entidad Autor.
*/
public interface AutorRepository extends JpaRepository<Autor, Long> {
// Busca autores cuyo nombre contenga el texto indicado.
List<Autor> findByNombreContainingIgnoreCase(String nombre);
// Busca autores por nacionalidad.
List<Autor> findByNacionalidadContainingIgnoreCase(String nacionalidad);
// Busca autores por fecha de nacimiento.
List<Autor> findByFechaNacimiento(LocalDate fechaNacimiento);
}
package es.pulsoft.spring_biblioteca.service;
import es.pulsoft.spring_biblioteca.dto.AutorCrearDto;
import es.pulsoft.spring_biblioteca.dto.AutorDto;
import es.pulsoft.spring_biblioteca.dto.AutorPatchDto;
import es.pulsoft.spring_biblioteca.mapper.AutorMapper;
import es.pulsoft.spring_biblioteca.model.Autor;
import es.pulsoft.spring_biblioteca.repository.AutorRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.util.List;
/**
* Servicio encargado de gestionar la lógica de negocio
* relacionada con los autores.
*/
@Service
public class AutorService {
// Repositorio para acceder a los datos de los autores.
private final AutorRepository autorRepository;
// Mapper para convertir entre entidades y DTOs.
private final AutorMapper autorMapper;
public AutorService(AutorRepository autorRepository, AutorMapper autorMapper) {
this.autorRepository = autorRepository;
this.autorMapper = autorMapper;
}
// Convierte una lista de entidades Autor en una lista de DTOs.
private List<AutorDto> convertirAutores(List<Autor> listaAutores) {
return listaAutores
.stream()
.map(autorMapper::toDto)
.toList();
}
// Busca un autor por su identificador o lanza una excepción si no existe.
public Autor buscarId(Long id) {
return autorRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Autor no encontrado"));
}
// Obtiene el listado completo de autores.
public List<AutorDto> listarAutores() {
return convertirAutores(autorRepository.findAll());
}
// Obtiene un autor por su identificador.
public AutorDto buscarPorId(Long id) {
return autorMapper.toDto(buscarId(id));
}
// Filtra autores por nombre, nacionalidad o fecha de nacimiento.
public List<AutorDto> filtrarAutores(String nombre, String nacionalidad, LocalDate fechaNacimiento) {
if (nombre != null && !nombre.isBlank()) {
return convertirAutores(autorRepository.findByNombreContainingIgnoreCase(nombre));
}
if (nacionalidad != null && !nacionalidad.isBlank()) {
return convertirAutores(autorRepository.findByNacionalidadContainingIgnoreCase(nacionalidad));
}
if (fechaNacimiento != null) {
return convertirAutores(autorRepository.findByFechaNacimiento(fechaNacimiento));
}
return convertirAutores(autorRepository.findAll());
}
// Crea un nuevo autor.
public AutorDto agregarAutor(AutorCrearDto dto) {
Autor autor = autorMapper.toEntity(dto);
Autor guardado = autorRepository.save(autor);
return autorMapper.toDto(guardado);
}
// Actualiza completamente un autor existente.
public AutorDto actualizarAutor(Long id, AutorCrearDto dto) {
Autor autor = buscarId(id);
autor.setNombre(dto.getNombre());
autor.setNacionalidad(dto.getNacionalidad());
autor.setFechaNacimiento(dto.getFechaNacimiento());
return autorMapper.toDto(autorRepository.save(autor));
}
// Actualiza parcialmente un autor existente.
public AutorDto actualizacionParcial(Long id, AutorPatchDto dto) {
Autor autor = buscarId(id);
autorMapper.update(autor, dto);
return autorMapper.toDto(autorRepository.save(autor));
}
// Elimina un autor por su identificador.
public void eliminarAutor(Long id) {
Autor autor = buscarId(id);
autorRepository.delete(autor);
}
}
Gestión de editoriales (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de editoriales 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 información de las editoriales de una biblioteca, incluyendo sus datos de contacto y la relación con los libros publicados.
Funcionalidades a implementar
Crear una editorial a partir de un DTO de entrada (EditorialCrearDto), validando que el nombre, la ciudad y el teléfono sean obligatorios, y que el teléfono tenga un formato válido.
Obtener el listado completo de editoriales registradas.
Obtener una editorial a partir de su identificador.
Filtrar editoriales por nombre, ciudad o teléfono.
Actualizar completamente los datos de una editorial existente.
Actualizar parcialmente una editorial mediante un DTO (EditorialPatchDto), permitiendo modificar únicamente los campos enviados en la petición.
Eliminar una editorial a partir de su identificador.
Utilizar DTOs de salida (EditorialDto) 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 editorial que no exista.
Gestionar la relación entre Editorial y Libro mediante JPA (@OneToMany).
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.
Implementación de mappers mediante MapStruct.
Gestión de relaciones entre entidades mediante JPA.
Separación de responsabilidades entre Controller, Service y Repository.
package es.pulsoft.spring_biblioteca.controller;
import es.pulsoft.spring_biblioteca.dto.EditorialCrearDto;
import es.pulsoft.spring_biblioteca.dto.EditorialDto;
import es.pulsoft.spring_biblioteca.dto.EditorialPatchDto;
import es.pulsoft.spring_biblioteca.service.EditorialService;
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 las editoriales.
*/
@RestController
@RequestMapping("/editoriales")
public class EditorialController {
// Servicio que contiene la lógica de negocio de las editoriales.
private final EditorialService editorialService;
public EditorialController(EditorialService editorialService) {
this.editorialService = editorialService;
}
// Obtiene el listado completo de editoriales.
@GetMapping
public ResponseEntity<List<EditorialDto>> listarEditoriales() {
return ResponseEntity.ok(editorialService.listarEditoriales());
}
// Obtiene una editorial por su identificador.
@GetMapping("/{id}")
public ResponseEntity<EditorialDto> buscarPorId(@PathVariable Long id) {
return ResponseEntity.ok(editorialService.buscarPorId(id));
}
// Filtra editoriales por nombre, ciudad o teléfono.
@GetMapping("/filtrar")
public ResponseEntity<List<EditorialDto>> filtrarEditoriales(
@RequestParam(required = false) String nombre,
@RequestParam(required = false) String ciudad,
@RequestParam(required = false) String telefono) {
return ResponseEntity.ok(editorialService.filtrarEditoriales(nombre, ciudad, telefono));
}
// Crea una nueva editorial.
@PostMapping
public ResponseEntity<EditorialDto> agregarEditorial(@Valid @RequestBody EditorialCrearDto dto) {
return ResponseEntity.ok(editorialService.agregarEditorial(dto));
}
// Actualiza completamente una editorial existente.
@PutMapping("/{id}")
public ResponseEntity<EditorialDto> actualizarEditorial(
@PathVariable Long id,
@Valid @RequestBody EditorialCrearDto dto) {
return ResponseEntity.ok(editorialService.actualizarEditorial(id, dto));
}
// Actualiza parcialmente una editorial existente.
@PatchMapping("/{id}")
public ResponseEntity<EditorialDto> actualizacionParcial(
@PathVariable Long id,
@Valid @RequestBody EditorialPatchDto dto) {
return ResponseEntity.ok(editorialService.actualizacionParcial(id, dto));
}
// Elimina una editorial por su identificador.
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminarEditorial(@PathVariable Long id) {
editorialService.eliminarEditorial(id);
return ResponseEntity.noContent().build();
}
}
package es.pulsoft.spring_biblioteca.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.*;
/**
* DTO utilizado para crear una nueva editorial.
* Contiene únicamente los datos necesarios para el alta.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EditorialCrearDto {
// Nombre de la editorial.
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
// Ciudad donde se encuentra la editorial.
@NotBlank(message = "La ciudad es obligatoria.")
private String ciudad;
// Teléfono de contacto de la editorial.
// Debe contener exactamente 9 cifras.
@NotBlank(message = "El telefono es obligatorio.")
@Pattern(
regexp = "\\d{9}",
message = "El teléfono debe tener exactamente 9 cifras."
)
private String telefono;
}
package es.pulsoft.spring_biblioteca.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* DTO utilizado para devolver la información de una editorial.
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EditorialDto {
// Identificador único de la editorial.
private Long id;
// Nombre de la editorial.
private String nombre;
// Ciudad donde se encuentra la editorial.
private String ciudad;
// Teléfono de contacto de la editorial.
private String telefono;
}
package es.pulsoft.spring_biblioteca.mapper;
import es.pulsoft.spring_biblioteca.dto.EditorialCrearDto;
import es.pulsoft.spring_biblioteca.dto.EditorialDto;
import es.pulsoft.spring_biblioteca.dto.EditorialPatchDto;
import es.pulsoft.spring_biblioteca.model.Editorial;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
/**
* Mapper encargado de convertir entre la entidad Editorial y sus distintos DTOs.
*/
@Mapper(componentModel = "spring")
public interface EditorialMapper {
/**
* Convierte un DTO de creación en una entidad Editorial.
*/
Editorial toEntity(EditorialCrearDto dto);
/**
* Convierte una entidad Editorial en un DTO de respuesta.
*/
EditorialDto toDto(Editorial editorial);
/**
* Actualiza parcialmente una entidad Editorial con los datos recibidos
* en el DTO, ignorando los campos con valor null.
*/
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Editorial editorial, EditorialPatchDto dto);
}
package es.pulsoft.spring_biblioteca.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
/**
* Entidad que representa una editorial dentro del sistema de biblioteca.
* Una editorial puede publicar varios libros.
*/
@Entity
@Table(name = "editorial")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Editorial {
// Identificador único de la editorial generado automáticamente.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Nombre comercial de la editorial.
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
// Ciudad donde está ubicada la editorial.
@NotBlank(message = "La ciudad es obligatoria.")
private String ciudad;
// Teléfono de contacto de la editorial.
// Debe contener exactamente 9 cifras.
@NotBlank(message = "El telefono es obligatorio.")
@Pattern(
regexp = "\\d{9}",
message = "El teléfono debe tener exactamente 9 cifras."
)
private String telefono;
// Relación uno a muchos: una editorial puede tener varios libros.
// La relación se gestiona desde la entidad Libro mediante el campo editorial.
@OneToMany(mappedBy = "editorial")
private List<Libro> libros;
}
package es.pulsoft.spring_biblioteca.dto;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* DTO utilizado para la actualización parcial de una editorial.
* Todos los campos son opcionales.
*/
@Getter
@Setter
@NoArgsConstructor
public class EditorialPatchDto {
// Nombre de la editorial.
private String nombre;
// Ciudad donde se encuentra la editorial.
private String ciudad;
// Teléfono de contacto de la editorial.
// Si se informa, debe contener exactamente 9 cifras.
@Pattern(
regexp = "\\d{9}",
message = "El teléfono debe tener exactamente 9 cifras."
)
private String telefono;
}
package es.pulsoft.spring_biblioteca.repository;
import es.pulsoft.spring_biblioteca.model.Editorial;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
/**
* Repositorio encargado de gestionar el acceso a los datos
* de la entidad Editorial.
*/
public interface EditorialRepository extends JpaRepository<Editorial, Long> {
// Busca editoriales cuyo nombre contenga el texto indicado.
List<Editorial> findByNombreContainingIgnoreCase(String nombre);
// Busca editoriales por ciudad.
List<Editorial> findByCiudadContainingIgnoreCase(String ciudad);
// Busca editoriales por teléfono.
List<Editorial> findByTelefono(String telefono);
}
package es.pulsoft.spring_biblioteca.service;
import es.pulsoft.spring_biblioteca.dto.EditorialCrearDto;
import es.pulsoft.spring_biblioteca.dto.EditorialDto;
import es.pulsoft.spring_biblioteca.dto.EditorialPatchDto;
import es.pulsoft.spring_biblioteca.mapper.EditorialMapper;
import es.pulsoft.spring_biblioteca.model.Editorial;
import es.pulsoft.spring_biblioteca.repository.EditorialRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
/**
* Servicio encargado de gestionar la lógica de negocio
* relacionada con las editoriales.
*/
@Service
public class EditorialService {
// Repositorio para acceder a los datos de las editoriales.
private final EditorialRepository editorialRepository;
// Mapper para convertir entre entidades y DTOs.
private final EditorialMapper editorialMapper;
public EditorialService(EditorialRepository editorialRepository, EditorialMapper editorialMapper) {
this.editorialRepository = editorialRepository;
this.editorialMapper = editorialMapper;
}
// Convierte una lista de entidades Editorial en una lista de DTOs.
private List<EditorialDto> convertirEditoriales(List<Editorial> listaEditoriales) {
return listaEditoriales
.stream()
.map(editorialMapper::toDto)
.toList();
}
// Busca una editorial por su identificador o lanza una excepción si no existe.
public Editorial buscarId(Long id) {
return editorialRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Editorial no encontrada"));
}
// Obtiene el listado completo de editoriales.
public List<EditorialDto> listarEditoriales() {
return convertirEditoriales(editorialRepository.findAll());
}
// Obtiene una editorial por su identificador.
public EditorialDto buscarPorId(Long id) {
return editorialMapper.toDto(buscarId(id));
}
// Filtra editoriales por nombre, ciudad o teléfono.
public List<EditorialDto> filtrarEditoriales(String nombre, String ciudad, String telefono) {
if (nombre != null && !nombre.isBlank()) {
return convertirEditoriales(editorialRepository.findByNombreContainingIgnoreCase(nombre));
}
if (ciudad != null && !ciudad.isBlank()) {
return convertirEditoriales(editorialRepository.findByCiudadContainingIgnoreCase(ciudad));
}
if (telefono != null && !telefono.isBlank()) {
return convertirEditoriales(editorialRepository.findByTelefono(telefono));
}
return convertirEditoriales(editorialRepository.findAll());
}
// Crea una nueva editorial.
public EditorialDto agregarEditorial(EditorialCrearDto dto) {
Editorial editorial = editorialMapper.toEntity(dto);
return editorialMapper.toDto(editorialRepository.save(editorial));
}
// Actualiza completamente una editorial existente.
public EditorialDto actualizarEditorial(Long id, EditorialCrearDto dto) {
Editorial editorial = buscarId(id);
editorial.setNombre(dto.getNombre());
editorial.setCiudad(dto.getCiudad());
editorial.setTelefono(dto.getTelefono());
return editorialMapper.toDto(editorialRepository.save(editorial));
}
// Actualiza parcialmente una editorial existente.
public EditorialDto actualizacionParcial(Long id, EditorialPatchDto dto) {
Editorial editorial = buscarId(id);
editorialMapper.update(editorial, dto);
return editorialMapper.toDto(editorialRepository.save(editorial));
}
// Elimina una editorial por su identificador.
public void eliminarEditorial(Long id) {
Editorial editorial = buscarId(id);
editorialRepository.delete(editorial);
}
}
Gestión de usuarios (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de usuarios 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 información de los usuarios de una biblioteca, incluyendo sus datos personales y la relación con los préstamos realizados.
Funcionalidades a implementar
Crear un usuario a partir de un DTO de entrada (UsuarioCrearDto), validando que el nombre y el correo electrónico sean obligatorios, que el email tenga un formato válido y que el teléfono tenga un formato correcto.
Obtener el listado completo de usuarios registrados.
Obtener un usuario a partir de su identificador.
Filtrar usuarios por nombre, correo electrónico o teléfono.
Actualizar completamente los datos de un usuario existente.
Actualizar parcialmente un usuario mediante un DTO (UsuarioPatchDto), permitiendo modificar únicamente los campos enviados en la petición.
Eliminar un usuario a partir de su identificador.
Utilizar DTOs de salida (UsuarioDto) 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 usuario que no exista.
Gestionar la relación entre Usuario y Préstamo mediante JPA (@OneToMany).
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.
Implementación de mappers mediante MapStruct.
Gestión de relaciones entre entidades mediante JPA.
Separación de responsabilidades entre Controller, Service y Repository.
package es.pulsoft.spring_biblioteca.controller;
import es.pulsoft.spring_biblioteca.dto.UsuarioCrearDto;
import es.pulsoft.spring_biblioteca.dto.UsuarioDto;
import es.pulsoft.spring_biblioteca.dto.UsuarioPatchDto;
import es.pulsoft.spring_biblioteca.service.UsuarioService;
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 usuarios.
*/
@RestController
@RequestMapping("/usuarios")
public class UsuarioController {
// Servicio que contiene la lógica de negocio de los usuarios.
private final UsuarioService usuarioService;
public UsuarioController(UsuarioService usuarioService) {
this.usuarioService = usuarioService;
}
// Obtiene el listado completo de usuarios.
@GetMapping
public ResponseEntity<List<UsuarioDto>> listarUsuarios() {
return ResponseEntity.ok(usuarioService.listarUsuarios());
}
// Obtiene un usuario por su identificador.
@GetMapping("/{id}")
public ResponseEntity<UsuarioDto> buscarPorId(@PathVariable Long id) {
return ResponseEntity.ok(usuarioService.buscarPorId(id));
}
// Filtra usuarios por nombre, email o teléfono.
@GetMapping("/filtrar")
public ResponseEntity<List<UsuarioDto>> filtrarUsuarios(
@RequestParam(required = false) String nombre,
@RequestParam(required = false) String email,
@RequestParam(required = false) String telefono) {
return ResponseEntity.ok(
usuarioService.filtrarUsuarios(nombre, email, telefono));
}
// Crea un nuevo usuario.
@PostMapping
public ResponseEntity<UsuarioDto> agregarUsuario(
@Valid @RequestBody UsuarioCrearDto dto) {
return ResponseEntity.ok(usuarioService.agregarUsuario(dto));
}
// Actualiza completamente un usuario existente.
@PutMapping("/{id}")
public ResponseEntity<UsuarioDto> actualizarUsuario(
@PathVariable Long id,
@Valid @RequestBody UsuarioCrearDto dto) {
return ResponseEntity.ok(usuarioService.actualizarUsuario(id, dto));
}
// Actualiza parcialmente un usuario existente.
@PatchMapping("/{id}")
public ResponseEntity<UsuarioDto> actualizacionParcial(
@PathVariable Long id,
@Valid @RequestBody UsuarioPatchDto dto) {
return ResponseEntity.ok(usuarioService.actualizacionParcial(id, dto));
}
// Elimina un usuario por su identificador.
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminarUsuario(@PathVariable Long id) {
usuarioService.eliminarUsuario(id);
return ResponseEntity.noContent().build();
}
}
package es.pulsoft.spring_biblioteca.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.*;
/**
* DTO utilizado para crear un nuevo usuario.
* Contiene únicamente los datos necesarios para el alta.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UsuarioCrearDto {
// Nombre completo del usuario.
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
// Correo electrónico del usuario.
@NotBlank(message = "El email es obligatorio.")
@Email(message = "Formato de email no válido.")
private String email;
// Teléfono de contacto del usuario.
// Debe contener exactamente 9 cifras.
@NotBlank(message = "El telefono es obligatorio.")
@Pattern(
regexp = "\\d{9}",
message = "El teléfono debe tener exactamente 9 cifras."
)
private String telefono;
}
package es.pulsoft.spring_biblioteca.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* DTO utilizado para devolver la información de un usuario.
* Incluye los datos personales y los libros asociados mediante préstamos.
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UsuarioDto {
// Identificador único del usuario.
private Long id;
// Nombre del usuario.
private String nombre;
// Correo electrónico del usuario.
private String email;
// Teléfono de contacto del usuario.
private String telefono;
// Lista de libros asociados a sus préstamos.
private List<String> libros;
}
package es.pulsoft.spring_biblioteca.mapper;
import es.pulsoft.spring_biblioteca.dto.UsuarioCrearDto;
import es.pulsoft.spring_biblioteca.dto.UsuarioDto;
import es.pulsoft.spring_biblioteca.dto.UsuarioPatchDto;
import es.pulsoft.spring_biblioteca.model.Prestamo;
import es.pulsoft.spring_biblioteca.model.Usuario;
import org.mapstruct.*;
import java.util.List;
/**
* Mapper encargado de convertir entre la entidad Usuario
* y sus diferentes DTOs.
*/
@Mapper(componentModel = "spring")
public interface UsuarioMapper {
/**
* Convierte un DTO de creación en una entidad Usuario.
*/
Usuario toEntity(UsuarioCrearDto dto);
/**
* Convierte una entidad Usuario en un DTO de respuesta.
* Obtiene la lista de libros asociados mediante sus préstamos.
*/
@Mapping(target = "libros", source = "prestamos")
UsuarioDto toDto(Usuario usuario);
/**
* Actualiza parcialmente una entidad Usuario ignorando campos null.
*/
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Usuario usuario, UsuarioPatchDto dto);
/**
* Convierte un préstamo en el título del libro asociado.
*/
default String mapPrestamoToLibro(Prestamo prestamo) {
return prestamo.getLibro().getTitulo();
}
}
package es.pulsoft.spring_biblioteca.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
/**
* Entidad que representa un usuario registrado en la biblioteca.
* Un usuario puede realizar varios préstamos de libros.
*/
@Entity
@Table(name = "usuario")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Usuario {
// Identificador único del usuario generado automáticamente.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Nombre completo del usuario.
@NotBlank(message = "El nombre es obligatorio.")
private String nombre;
// Correo electrónico del usuario.
@NotBlank(message = "El email es obligatorio.")
@Email(message = "Formato de email no válido.")
private String email;
// Teléfono de contacto del usuario.
// Debe contener exactamente 9 cifras.
@NotBlank(message = "El telefono es obligatorio.")
@Pattern(
regexp = "\\d{9}",
message = "El teléfono debe tener exactamente 9 cifras."
)
private String telefono;
// Relación uno a muchos: un usuario puede tener varios préstamos.
// La relación se gestiona desde Prestamo mediante el campo usuario.
@OneToMany(mappedBy = "usuario")
private List<Prestamo> prestamos;
}
package es.pulsoft.spring_biblioteca.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* DTO utilizado para la actualización parcial de un usuario.
* Todos los campos son opcionales.
*/
@Getter
@Setter
@NoArgsConstructor
public class UsuarioPatchDto {
// Nombre del usuario.
private String nombre;
// Correo electrónico del usuario.
// Si se informa, debe tener un formato válido.
@Email(message = "Formato de email no válido.")
private String email;
// Teléfono de contacto del usuario.
// Si se informa, debe contener exactamente 9 cifras.
@Pattern(
regexp = "\\d{9}",
message = "El teléfono debe tener exactamente 9 cifras."
)
private String telefono;
}
package es.pulsoft.spring_biblioteca.repository;
import es.pulsoft.spring_biblioteca.model.Usuario;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
/**
* Repositorio encargado de gestionar el acceso a los datos
* de la entidad Usuario.
*/
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
// Busca usuarios cuyo nombre contenga el texto indicado.
List<Usuario> findByNombreContainingIgnoreCase(String nombre);
// Busca usuarios por email.
List<Usuario> findByEmail(String email);
// Busca usuarios por teléfono.
List<Usuario> findByTelefono(String telefono);
}
package es.pulsoft.spring_biblioteca.service;
import es.pulsoft.spring_biblioteca.dto.UsuarioCrearDto;
import es.pulsoft.spring_biblioteca.dto.UsuarioDto;
import es.pulsoft.spring_biblioteca.dto.UsuarioPatchDto;
import es.pulsoft.spring_biblioteca.mapper.UsuarioMapper;
import es.pulsoft.spring_biblioteca.model.Usuario;
import es.pulsoft.spring_biblioteca.repository.UsuarioRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
/**
* Servicio encargado de gestionar la lógica de negocio
* relacionada con los usuarios.
*/
@Service
public class UsuarioService {
// Repositorio para acceder a los datos de los usuarios.
private final UsuarioRepository usuarioRepository;
// Mapper para convertir entre entidades y DTOs.
private final UsuarioMapper usuarioMapper;
public UsuarioService(UsuarioRepository usuarioRepository, UsuarioMapper usuarioMapper) {
this.usuarioRepository = usuarioRepository;
this.usuarioMapper = usuarioMapper;
}
// Convierte una lista de entidades Usuario en una lista de DTOs.
private List<UsuarioDto> convertirUsuarios(List<Usuario> listaUsuarios) {
return listaUsuarios
.stream()
.map(usuarioMapper::toDto)
.toList();
}
// Busca un usuario por su identificador o lanza una excepción si no existe.
public Usuario buscarId(Long id) {
return usuarioRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Usuario no encontrado"));
}
// Obtiene el listado completo de usuarios.
public List<UsuarioDto> listarUsuarios() {
return convertirUsuarios(usuarioRepository.findAll());
}
// Obtiene un usuario por su identificador.
public UsuarioDto buscarPorId(Long id) {
return usuarioMapper.toDto(buscarId(id));
}
// Filtra usuarios por nombre, email o teléfono.
public List<UsuarioDto> filtrarUsuarios(String nombre, String email, String telefono) {
if (nombre != null && !nombre.isBlank()) {
return convertirUsuarios(usuarioRepository.findByNombreContainingIgnoreCase(nombre));
}
if (email != null && !email.isBlank()) {
return convertirUsuarios(usuarioRepository.findByEmail(email));
}
if (telefono != null && !telefono.isBlank()) {
return convertirUsuarios(usuarioRepository.findByTelefono(telefono));
}
return convertirUsuarios(usuarioRepository.findAll());
}
// Crea un nuevo usuario.
public UsuarioDto agregarUsuario(UsuarioCrearDto dto) {
Usuario usuario = usuarioMapper.toEntity(dto);
return usuarioMapper.toDto(usuarioRepository.save(usuario));
}
// Actualiza completamente un usuario existente.
public UsuarioDto actualizarUsuario(Long id, UsuarioCrearDto dto) {
Usuario usuario = buscarId(id);
usuario.setNombre(dto.getNombre());
usuario.setEmail(dto.getEmail());
usuario.setTelefono(dto.getTelefono());
return usuarioMapper.toDto(usuarioRepository.save(usuario));
}
// Actualiza parcialmente un usuario existente.
public UsuarioDto actualizacionParcial(Long id, UsuarioPatchDto dto) {
Usuario usuario = buscarId(id);
usuarioMapper.update(usuario, dto);
return usuarioMapper.toDto(usuarioRepository.save(usuario));
}
// Elimina un usuario por su identificador.
public void eliminarUsuario(Long id) {
Usuario usuario = buscarId(id);
usuarioRepository.delete(usuario);
}
}
Gestión de libros (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de libros 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 información de los libros de una biblioteca, incluyendo sus datos principales, la relación con autores y editoriales, y su disponibilidad dentro del sistema.
Funcionalidades a implementar
Crear un libro a partir de un DTO de entrada (LibroCrearDto), validando que el título, el ISBN, la fecha de publicación y el número de páginas sean obligatorios, que el ISBN tenga un formato válido, que la fecha de publicación no sea futura y que el número de páginas sea mayor que cero.
Obtener el listado completo de libros registrados.
Obtener un libro a partir de su identificador.
Filtrar libros por título, ISBN, disponibilidad, autor o editorial.
Actualizar completamente los datos de un libro existente.
Actualizar parcialmente un libro mediante un DTO (LibroPatchDto), permitiendo modificar únicamente los campos enviados en la petición.
Eliminar un libro a partir de su identificador.
Utilizar DTOs de salida (LibroDto) 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 libro que no exista.
Gestionar la relación entre Libro, Autor y Editorial mediante JPA (@ManyToOne).
Gestionar la asociación del libro con los préstamos realizados mediante JPA.
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.
Implementación de mappers mediante MapStruct.
Gestión de relaciones entre entidades mediante JPA.
Separación de responsabilidades entre Controller, Service y Repository.
Uso de consultas derivadas de Spring Data JPA para realizar búsquedas y filtros.
package es.pulsoft.spring_biblioteca.controller;
import es.pulsoft.spring_biblioteca.dto.LibroCrearDto;
import es.pulsoft.spring_biblioteca.dto.LibroDto;
import es.pulsoft.spring_biblioteca.dto.LibroPatchDto;
import es.pulsoft.spring_biblioteca.service.LibroService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/libros")
public class LibroController {
// Servicio encargado de la lógica de negocio de los libros.
private final LibroService libroService;
public LibroController(LibroService libroService) {
this.libroService = libroService;
}
// Obtiene el listado completo de libros.
@GetMapping
public ResponseEntity<List<LibroDto>> listarLibros(){
return ResponseEntity.ok(libroService.listarLibros());
}
// Obtiene un libro a partir de su identificador.
@GetMapping("/{id}")
public ResponseEntity<LibroDto> buscarPorId(@PathVariable Long id){
return ResponseEntity.ok(libroService.buscarPorId(id));
}
// Filtra libros por los criterios indicados.
@GetMapping("/filtrar")
public ResponseEntity<List<LibroDto>> filtrarLibros(@RequestParam(required = false) String titulo,
@RequestParam(required = false) String isbn,
@RequestParam(required = false) LocalDate fechaPublicacion,
@RequestParam(required = false) Integer paginas,
@RequestParam(required = false) Boolean disponible){
return ResponseEntity.ok(libroService.filtrarLibros(
titulo, isbn, fechaPublicacion, paginas, disponible));
}
// Crea un nuevo libro.
@PostMapping
public ResponseEntity<LibroDto> agregarLibro(@Valid @RequestBody LibroCrearDto dto){
return ResponseEntity.ok(libroService.agregarLibro(dto));
}
// Actualiza completamente un libro existente.
@PutMapping("/{id}")
public ResponseEntity<LibroDto> actualizarLibro(@PathVariable Long id,
@Valid @RequestBody LibroCrearDto dto){
return ResponseEntity.ok(libroService.actualizarLibro(id, dto));
}
// Actualiza parcialmente un libro existente.
@PatchMapping("/{id}")
public ResponseEntity<LibroDto> actualizacionParcial(@PathVariable Long id,
@Valid @RequestBody LibroPatchDto dto){
return ResponseEntity.ok(libroService.actualizacionParcial(id, dto));
}
// Elimina un libro por su identificador.
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminarLibro(@PathVariable Long id){
libroService.eliminarLibro(id);
return ResponseEntity.noContent().build();
}
}
package es.pulsoft.spring_biblioteca.dto;
import jakarta.validation.constraints.*;
import lombok.*;
import java.time.LocalDate;
/**
* DTO utilizado para crear un nuevo libro.
* Contiene los datos necesarios para el alta del libro
* junto con las referencias del autor y la editorial.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LibroCrearDto {
// Título del libro.
@NotBlank(message = "El titulo es obligatorio.")
private String titulo;
// Código ISBN del libro.
@NotBlank(message = "El isbn es obligatorio.")
@Size(min = 10, max = 13, message = "El ISBN debe tener entre 10 y 13 caracteres.")
private String isbn;
// Fecha de publicación del libro.
// No puede ser una fecha futura.
@NotNull(message = "La fecha de publicación es obligatoria.")
@PastOrPresent(message = "La fecha no puede ser futura.")
private LocalDate fechaPublicacion;
// Número de páginas del libro.
@NotNull(message = "El numero de paginas es obligatorio.")
@Min(value = 1, message = "El numero de páginas no puede ser inferior a 1.")
private Integer paginas;
// Identificador del autor asociado al libro.
@NotNull(message = "El autor es obligatorio.")
private Long autorId;
// Identificador de la editorial asociada al libro.
@NotNull(message = "La editorial es obligatoria.")
private Long editorialId;
}
package es.pulsoft.spring_biblioteca.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 un libro.
* Incluye datos básicos y la información relacionada del autor y editorial.
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LibroDto {
// Identificador único del libro.
private Long id;
// Título del libro.
private String titulo;
// Código ISBN del libro.
private String isbn;
// Fecha de publicación del libro.
private LocalDate fechaPublicacion;
// Número de páginas del libro.
private Integer paginas;
// Indica si el libro está disponible para préstamo.
private Boolean disponible;
// Nombre del autor asociado al libro.
private String autor;
// Nombre de la editorial asociada al libro.
private String editorial;
}
package es.pulsoft.spring_biblioteca.mapper;
import es.pulsoft.spring_biblioteca.dto.LibroCrearDto;
import es.pulsoft.spring_biblioteca.dto.LibroDto;
import es.pulsoft.spring_biblioteca.dto.LibroPatchDto;
import es.pulsoft.spring_biblioteca.model.Libro;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
/**
* Mapper encargado de convertir entre la entidad Libro y sus DTOs.
*/
@Mapper(componentModel = "spring")
public interface LibroMapper {
/**
* Convierte un DTO de creación en una entidad Libro.
*/
Libro toEntity(LibroCrearDto dto);
/**
* Convierte una entidad Libro en un DTO de respuesta.
* Obtiene el nombre del autor y la editorial asociados.
*/
@Mapping(target = "autor", source = "autor.nombre")
@Mapping(target = "editorial", source = "editorial.nombre")
LibroDto toDto(Libro libro);
/**
* Actualiza parcialmente una entidad Libro ignorando campos null.
*/
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Libro libro, LibroPatchDto dto);
}
package es.pulsoft.spring_biblioteca.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDate;
import java.util.List;
/**
* Entidad que representa un libro dentro de la biblioteca.
* Un libro pertenece a un autor y una editorial, y puede tener
* varios préstamos asociados.
*/
@Entity
@Table(name = "libro")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Libro {
// Identificador único del libro generado automáticamente.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Título del libro.
@NotBlank(message = "El titulo es obligatorio.")
private String titulo;
// Código ISBN del libro.
@NotBlank(message = "El isbn es obligatorio.")
@Size(min = 10, max = 13, message = "El ISBN debe tener entre 10 y 13 caracteres.")
private String isbn;
// Fecha de publicación del libro. No puede ser futura.
@NotNull(message = "La fecha de publicación es obligatoria.")
@PastOrPresent(message = "La fecha no puede ser futura.")
private LocalDate fechaPublicacion;
// Número de páginas del libro.
@NotNull(message = "El numero de paginas es obligatorio.")
@Min(value = 1, message = "El numero de páginas no puede ser inferior a 1.")
private Integer paginas;
// Indica si el libro está disponible para préstamo.
// Por defecto los libros nuevos estarán disponibles.
private Boolean disponible = true;
// Relación muchos a uno: varios libros pueden pertenecer al mismo autor.
@ManyToOne(optional = false)
@JoinColumn(name = "autor_id", nullable = false)
private Autor autor;
// Relación muchos a uno: varias publicaciones pueden pertenecer a una editorial.
@ManyToOne(optional = false)
@JoinColumn(name = "editorial_id", nullable = false)
private Editorial editorial;
// Relación uno a muchos: un libro puede tener varios préstamos.
@OneToMany(mappedBy = "libro")
private List<Prestamo> prestamos;
}
package es.pulsoft.spring_biblioteca.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.PastOrPresent;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDate;
/**
* DTO utilizado para la actualización parcial de un libro.
* Todos los campos son opcionales.
*/
@Getter
@Setter
@NoArgsConstructor
public class LibroPatchDto {
// Título del libro.
private String titulo;
// Código ISBN del libro.
// Si se informa, debe tener entre 10 y 13 caracteres.
@Size(min = 10, max = 13, message = "El ISBN debe tener entre 10 y 13 caracteres.")
private String isbn;
// Fecha de publicación del libro.
// Si se informa, no puede ser futura.
@PastOrPresent(message = "La fecha no puede ser futura.")
private LocalDate fechaPublicacion;
// Número de páginas del libro.
// Si se informa, debe ser como mínimo 1.
@Min(value = 1, message = "El numero de páginas no puede ser inferior a 1.")
private Integer paginas;
// Indica si el libro está disponible para préstamo.
private Boolean disponible;
private Long autorId;
private Long editorialId;
}
package es.pulsoft.spring_biblioteca.repository;
import es.pulsoft.spring_biblioteca.model.Libro;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
/**
* Repositorio encargado de gestionar las operaciones de acceso a datos
* de la entidad Libro.
*/
public interface LibroRepository extends JpaRepository<Libro, Long> {
// Busca libros cuyo título contenga el texto indicado ignorando mayúsculas y minúsculas.
List<Libro> findByTituloContainingIgnoreCase(String titulo);
// Busca libros cuyo ISBN contenga el texto indicado ignorando mayúsculas y minúsculas.
List<Libro> findByIsbnContainingIgnoreCase(String isbn);
// Busca libros por fecha exacta de publicación.
List<Libro> findByFechaPublicacion(LocalDate fechaPublicacion);
// Busca libros por número exacto de páginas.
List<Libro> findByPaginas(Integer paginas);
// Busca libros que estén disponibles para préstamo.
List<Libro> findByDisponibleTrue();
// Busca libros que no estén disponibles para préstamo.
List<Libro> findByDisponibleFalse();
}
package es.pulsoft.spring_biblioteca.service;
import es.pulsoft.spring_biblioteca.dto.LibroCrearDto;
import es.pulsoft.spring_biblioteca.dto.LibroDto;
import es.pulsoft.spring_biblioteca.dto.LibroPatchDto;
import es.pulsoft.spring_biblioteca.mapper.LibroMapper;
import es.pulsoft.spring_biblioteca.model.Autor;
import es.pulsoft.spring_biblioteca.model.Editorial;
import es.pulsoft.spring_biblioteca.model.Libro;
import es.pulsoft.spring_biblioteca.repository.LibroRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.util.List;
@Service
public class LibroService {
// Repositorio encargado del acceso a datos de los libros.
private final LibroRepository libroRepository;
// Mapper para convertir entre entidades y DTOs.
private final LibroMapper libroMapper;
// Servicio utilizado para gestionar los autores asociados.
private final AutorService autorService;
// Servicio utilizado para gestionar las editoriales asociadas.
private final EditorialService editorialService;
public LibroService(LibroRepository libroRepository, LibroMapper libroMapper,
AutorService autorService, EditorialService editorialService) {
this.libroRepository = libroRepository;
this.libroMapper = libroMapper;
this.autorService = autorService;
this.editorialService = editorialService;
}
// Convierte una lista de entidades Libro en una lista de DTOs.
private List<LibroDto> convertirLibros(List<Libro> listaLibros) {
return listaLibros
.stream()
.map(libroMapper::toDto)
.toList();
}
// Busca un libro por su identificador o lanza una excepción si no existe.
public Libro buscarId(Long id) {
return libroRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Libro no encontrado"));
}
// Devuelve el listado completo de libros.
public List<LibroDto> listarLibros() {
return convertirLibros(libroRepository.findAll());
}
// Obtiene un libro por su identificador.
public LibroDto buscarPorId(Long id) {
return libroMapper.toDto(buscarId(id));
}
// Filtra los libros según el criterio recibido.
public List<LibroDto> filtrarLibros(String titulo,
String isbn,
LocalDate fechaPublicacion,
Integer paginas,
Boolean disponible) {
if (titulo != null && !titulo.isBlank()) {
return convertirLibros(libroRepository.findByTituloContainingIgnoreCase(titulo));
}
if (isbn != null && !isbn.isBlank()) {
return convertirLibros(libroRepository.findByIsbnContainingIgnoreCase(isbn));
}
if (fechaPublicacion != null) {
return convertirLibros(libroRepository.findByFechaPublicacion(fechaPublicacion));
}
if (paginas != null) {
return convertirLibros(libroRepository.findByPaginas(paginas));
}
if (disponible != null) {
if (disponible) {
return convertirLibros(libroRepository.findByDisponibleTrue());
} else {
return convertirLibros(libroRepository.findByDisponibleFalse());
}
}
return convertirLibros(libroRepository.findAll());
}
// Crea un nuevo libro y establece su autor y editorial.
public LibroDto agregarLibro(LibroCrearDto dto) {
Libro libro = libroMapper.toEntity(dto);
libro.setAutor(autorService.buscarId(dto.getAutorId()));
libro.setEditorial(editorialService.buscarId(dto.getEditorialId()));
return libroMapper.toDto(libroRepository.save(libro));
}
// Actualiza completamente un libro existente.
public LibroDto actualizarLibro(Long id, LibroCrearDto dto) {
Libro libro = buscarId(id);
Autor autor = autorService.buscarId(dto.getAutorId());
Editorial editorial = editorialService.buscarId(dto.getEditorialId());
libro.setTitulo(dto.getTitulo());
libro.setIsbn(dto.getIsbn());
libro.setFechaPublicacion(dto.getFechaPublicacion());
libro.setPaginas(dto.getPaginas());
libro.setAutor(autor);
libro.setEditorial(editorial);
return libroMapper.toDto(libroRepository.save(libro));
}
// Actualiza parcialmente un libro existente.
public LibroDto actualizacionParcial(Long id, LibroPatchDto dto) {
Libro libro = buscarId(id);
libroMapper.update(libro, dto);
if (dto.getAutorId() != null) {
libro.setAutor(autorService.buscarId(dto.getAutorId()));
}
if (dto.getEditorialId() != null) {
libro.setEditorial(editorialService.buscarId(dto.getEditorialId()));
}
return libroMapper.toDto(libroRepository.save(libro));
}
// Elimina un libro por su identificador.
public void eliminarLibro(Long id) {
Libro libro = buscarId(id);
libroRepository.delete(libro);
}
}
Gestión de préstamos (API REST con Spring Boot)
Se pide desarrollar una API REST para la gestión de préstamos 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 los préstamos realizados en una biblioteca, controlando la relación entre usuarios y libros, así como las fechas asociadas al préstamo y devolución.
Funcionalidades a implementar
Crear un préstamo a partir de un DTO de entrada (PrestamoCrearDto), validando que la fecha de préstamo, el usuario y el libro sean obligatorios.
Validar que la fecha de devolución no sea anterior a la fecha del préstamo.
Obtener el listado completo de préstamos registrados.
Obtener un préstamo a partir de su identificador.
Filtrar préstamos por fecha de préstamo, fecha de devolución, usuario o libro.
Actualizar completamente los datos de un préstamo existente.
Actualizar parcialmente un préstamo mediante un DTO (PrestamoPatchDto), permitiendo modificar únicamente los campos enviados en la petición.
Eliminar un préstamo a partir de su identificador.
Utilizar DTOs de salida (PrestamoDto) 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 préstamo que no exista.
Gestionar la relación entre Usuario y Préstamo mediante JPA (@ManyToOne).
Gestionar la relación entre Libro y Préstamo mediante JPA (@ManyToOne).
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.
Implementación de mappers mediante MapStruct.
Gestión de relaciones entre entidades mediante JPA.
Separación de responsabilidades entre Controller, Service y Repository.
Uso de consultas derivadas de Spring Data JPA para realizar búsquedas y filtros.
package es.pulsoft.spring_biblioteca.controller;
import es.pulsoft.spring_biblioteca.dto.PrestamoCrearDto;
import es.pulsoft.spring_biblioteca.dto.PrestamoDto;
import es.pulsoft.spring_biblioteca.dto.PrestamoPatchDto;
import es.pulsoft.spring_biblioteca.service.PrestamoService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/prestamos")
public class PrestamoController {
// Servicio encargado de gestionar las operaciones de préstamos
private final PrestamoService prestamoService;
// Inyección de dependencias mediante constructor
public PrestamoController(PrestamoService prestamoService) {
this.prestamoService = prestamoService;
}
// Obtiene el listado completo de préstamos
@GetMapping
public ResponseEntity<List<PrestamoDto>> listarPrestamos(){
return ResponseEntity.ok(prestamoService.listarPrestamos());
}
// Busca un préstamo por su identificador
@GetMapping("/{id}")
public ResponseEntity<PrestamoDto> buscarPorId(@PathVariable Long id){
return ResponseEntity.ok(prestamoService.buscarPorId(id));
}
// Filtra préstamos mediante diferentes parámetros opcionales
@GetMapping("/filtrar")
public ResponseEntity<List<PrestamoDto>> filtrarPrestamos(@RequestParam(required = false) LocalDate fechaPrestamo,
@RequestParam(required = false) LocalDate fechaDevolucion,
@RequestParam(required = false) Long usuarioId,
@RequestParam(required = false) Long libroId){
return ResponseEntity.ok(prestamoService.filtrarPrestamos(fechaPrestamo, fechaDevolucion, usuarioId, libroId));
}
// Crea un nuevo préstamo validando los datos recibidos
@PostMapping
public ResponseEntity<PrestamoDto> agregarPrestamo(@Valid @RequestBody PrestamoCrearDto dto){
return ResponseEntity.ok(prestamoService.agregarPrestamo(dto));
}
// Actualiza completamente los datos de un préstamo existente
@PutMapping("/{id}")
public ResponseEntity<PrestamoDto> actualizarPrestamo(@PathVariable Long id,
@Valid @RequestBody PrestamoCrearDto dto){
return ResponseEntity.ok(prestamoService.actualizarPrestamo(id, dto));
}
// Actualiza parcialmente los datos de un préstamo
@PatchMapping("/{id}")
public ResponseEntity<PrestamoDto> actualizacionParcial(@PathVariable Long id,
@Valid @RequestBody PrestamoPatchDto dto){
return ResponseEntity.ok(prestamoService.actualizacionParcial(id, dto));
}
// Elimina un préstamo mediante su identificador
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminarPrestamo(@PathVariable Long id){
prestamoService.eliminarPrestamo(id);
return ResponseEntity.noContent().build();
}
}
package es.pulsoft.spring_biblioteca.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import lombok.*;
import java.time.LocalDate;
/**
* DTO utilizado para crear un nuevo préstamo.
* Contiene las fechas del préstamo y las referencias
* del usuario y libro asociados.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PrestamoCrearDto {
// Fecha en la que se realiza el préstamo.
// No puede ser una fecha futura.
@NotNull(message = "La fecha de préstamo es obligatoria.")
@PastOrPresent(message = "La fecha de préstamo no puede ser futura.")
private LocalDate fechaPrestamo;
// Fecha en la que se devuelve el libro.
// Puede ser null si el préstamo sigue activo.
@PastOrPresent(message = "La fecha de devolución no puede ser futura.")
private LocalDate fechaDevolucion;
// Identificador del usuario que realiza el préstamo.
@NotNull(message = "El usuario es obligatorio.")
private Long usuarioId;
// Identificador del libro prestado.
@NotNull(message = "El libro es obligatorio.")
private Long libroId;
}
package es.pulsoft.spring_biblioteca.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 un préstamo.
* Incluye los datos del usuario y libro asociados.
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PrestamoDto {
// Identificador único del préstamo.
private Long id;
// Fecha en la que se realizó el préstamo.
private LocalDate fechaPrestamo;
// Fecha en la que se devolvió el libro.
private LocalDate fechaDevolucion;
// Nombre del usuario que realizó el préstamo.
private String usuario;
// Título del libro prestado.
private String libro;
}
package es.pulsoft.spring_biblioteca.mapper;
import es.pulsoft.spring_biblioteca.dto.PrestamoCrearDto;
import es.pulsoft.spring_biblioteca.dto.PrestamoDto;
import es.pulsoft.spring_biblioteca.dto.PrestamoPatchDto;
import es.pulsoft.spring_biblioteca.model.Prestamo;
import org.mapstruct.*;
/**
* Mapper encargado de convertir entre la entidad Prestamo
* y sus diferentes DTOs.
*/
@Mapper(componentModel = "spring")
public interface PrestamoMapper {
/**
* Convierte un DTO de creación en una entidad Prestamo.
*/
Prestamo toEntity(PrestamoCrearDto dto);
/**
* Convierte una entidad Prestamo en un DTO de respuesta.
* Obtiene el nombre del usuario y el título del libro asociado.
*/
@Mapping(target = "usuario", source = "usuario.nombre")
@Mapping(target = "libro", source = "libro.titulo")
PrestamoDto toDto(Prestamo prestamo);
/**
* Actualiza parcialmente una entidad Prestamo ignorando campos null.
*/
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void update(@MappingTarget Prestamo prestamo, PrestamoPatchDto dto);
}
package es.pulsoft.spring_biblioteca.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDate;
/**
* Entidad que representa un préstamo realizado en la biblioteca.
* Relaciona un usuario con un libro y controla las fechas
* de préstamo y devolución.
*/
@Entity
@Table(name = "prestamo")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Prestamo {
// Identificador único del préstamo generado automáticamente.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Fecha en la que se realiza el préstamo.
// No puede ser una fecha futura.
@NotNull(message = "La fecha de préstamo es obligatoria.")
@PastOrPresent(message = "La fecha de préstamo no puede ser futura.")
private LocalDate fechaPrestamo;
// Fecha en la que se devuelve el libro.
// Puede ser null mientras el préstamo siga activo.
@PastOrPresent(message = "La fecha de devolución no puede ser futura.")
private LocalDate fechaDevolucion;
// Relación muchos a uno: un usuario puede tener varios préstamos.
@ManyToOne(optional = false)
@JoinColumn(name = "usuario_id", nullable = false)
private Usuario usuario;
// Relación muchos a uno: un libro puede aparecer en varios préstamos.
@ManyToOne(optional = false)
@JoinColumn(name = "libro_id", nullable = false)
private Libro libro;
}
package es.pulsoft.spring_biblioteca.dto;
import jakarta.validation.constraints.PastOrPresent;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDate;
/**
* DTO utilizado para la actualización parcial de un préstamo.
* Todos los campos son opcionales.
*/
@Getter
@Setter
@NoArgsConstructor
public class PrestamoPatchDto {
// Fecha en la que se realizó el préstamo.
// Si se informa, no puede ser futura.
@PastOrPresent(message = "La fecha de préstamo no puede ser futura.")
private LocalDate fechaPrestamo;
// Fecha en la que se devuelve el libro.
// Si se informa, no puede ser futura.
@PastOrPresent(message = "La fecha de devolución no puede ser futura.")
private LocalDate fechaDevolucion;
private Long usuarioId;
private Long libroId;
}
package es.pulsoft.spring_biblioteca.repository;
import es.pulsoft.spring_biblioteca.model.Prestamo;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
// Repositorio para gestionar las operaciones de acceso a datos de préstamos
public interface PrestamoRepository extends JpaRepository<Prestamo, Long>{
// Busca préstamos por fecha de inicio
List<Prestamo> findByFechaPrestamo(LocalDate fechaPrestamo);
// Busca préstamos por fecha de devolución
List<Prestamo> findByFechaDevolucion(LocalDate fechaDevolucion);
// Busca préstamos asociados a un usuario
List<Prestamo> findByUsuarioId(Long usuarioId);
// Busca préstamos asociados a un libro
List<Prestamo> findByLibroId(Long libroId);
}
package es.pulsoft.spring_biblioteca.service;
import es.pulsoft.spring_biblioteca.dto.PrestamoCrearDto;
import es.pulsoft.spring_biblioteca.dto.PrestamoDto;
import es.pulsoft.spring_biblioteca.dto.PrestamoPatchDto;
import es.pulsoft.spring_biblioteca.mapper.PrestamoMapper;
import es.pulsoft.spring_biblioteca.model.Libro;
import es.pulsoft.spring_biblioteca.model.Prestamo;
import es.pulsoft.spring_biblioteca.model.Usuario;
import es.pulsoft.spring_biblioteca.repository.PrestamoRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.util.List;
@Service
public class PrestamoService {
// Repositorio encargado del acceso a datos de préstamos
private final PrestamoRepository prestamoRepository;
// Mapper encargado de convertir entre entidades y DTOs
private final PrestamoMapper prestamoMapper;
// Servicio para gestionar las operaciones relacionadas con usuarios
private final UsuarioService usuarioService;
// Servicio para gestionar las operaciones relacionadas con libros
private final LibroService libroService;
// Inyección de dependencias mediante constructor
public PrestamoService(PrestamoRepository prestamoRepository, PrestamoMapper prestamoMapper, UsuarioService usuarioService, LibroService libroService) {
this.prestamoRepository = prestamoRepository;
this.prestamoMapper = prestamoMapper;
this.usuarioService = usuarioService;
this.libroService = libroService;
}
// Valida que la fecha de devolución sea posterior a la fecha del préstamo
private void validarFechas(Prestamo prestamo){
if(prestamo.getFechaDevolucion()!=null &&
prestamo.getFechaDevolucion().isBefore(prestamo.getFechaPrestamo())){
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"La fecha de devolución no puede ser anterior a la fecha del préstamo"
);
}
}
// Convierte una lista de entidades Prestamo en una lista de DTOs
private List<PrestamoDto> convertirPrestamos(List<Prestamo> listaPrestamos){
return listaPrestamos
.stream()
.map(prestamoMapper::toDto)
.toList();
}
// Busca un préstamo por identificador o lanza excepción si no existe
private Prestamo buscarId(Long id){
return prestamoRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Prestamo no encontrado"));
}
// Obtiene todos los préstamos registrados
public List<PrestamoDto> listarPrestamos(){
return convertirPrestamos(prestamoRepository.findAll());
}
// Busca un préstamo por su identificador
public PrestamoDto buscarPorId(Long id){
return prestamoMapper.toDto(buscarId(id));
}
// Filtra préstamos utilizando diferentes criterios opcionales
public List<PrestamoDto> filtrarPrestamos(LocalDate fechaPrestamo,
LocalDate fechaDevolucion,
Long usuarioId,
Long libroId){
if(fechaPrestamo!=null){
return convertirPrestamos(prestamoRepository.findByFechaPrestamo(fechaPrestamo));
}
if(fechaDevolucion!=null){
return convertirPrestamos(prestamoRepository.findByFechaDevolucion(fechaDevolucion));
}
if(usuarioId!=null){
return convertirPrestamos(prestamoRepository.findByUsuarioId(usuarioId));
}
if(libroId!=null){
return convertirPrestamos(prestamoRepository.findByLibroId(libroId));
}
return convertirPrestamos(prestamoRepository.findAll());
}
// Crea un nuevo préstamo asociando usuario y libro
public PrestamoDto agregarPrestamo(PrestamoCrearDto dto){
Prestamo prestamo=prestamoMapper.toEntity(dto);
Usuario usuario=usuarioService.buscarId(dto.getUsuarioId());
Libro libro=libroService.buscarId(dto.getLibroId());
prestamo.setUsuario(usuario);
prestamo.setLibro(libro);
validarFechas(prestamo);
return prestamoMapper.toDto(prestamoRepository.save(prestamo));
}
// Actualiza completamente los datos de un préstamo existente
public PrestamoDto actualizarPrestamo(Long id, PrestamoCrearDto dto){
Prestamo prestamo=buscarId(id);
Libro libro=libroService.buscarId(dto.getLibroId());
Usuario usuario=usuarioService.buscarId(dto.getUsuarioId());
prestamo.setLibro(libro);
prestamo.setUsuario(usuario);
validarFechas(prestamo);
return prestamoMapper.toDto(prestamoRepository.save(prestamo));
}
// Actualiza parcialmente los datos de un préstamo existente
public PrestamoDto actualizacionParcial(Long id, PrestamoPatchDto dto){
Prestamo prestamo=buscarId(id);
prestamoMapper.update(prestamo,dto);
if(dto.getLibroId()!=null){
prestamo.setLibro(libroService.buscarId(dto.getLibroId()));
}
if(dto.getUsuarioId()!=null){
prestamo.setUsuario(usuarioService.buscarId(dto.getUsuarioId()));
}
validarFechas(prestamo);
return prestamoMapper.toDto(prestamoRepository.save(prestamo));
}
// Elimina un préstamo existente mediante su identificador
public void eliminarPrestamo(Long id){
Prestamo prestamo=buscarId(id);
prestamoRepository.delete(prestamo);
}
}