Semana 9 - Servicios en Spring Boot (Conceptos Básicos)
Resumen ejecutivo
Los servicios en Spring Boot contienen la lógica de negocio de la aplicación. Actúan como una capa intermedia entre controladores y repositorios, implementando reglas de negocio y validaciones.
1. ¿Qué es un servicio?
Definición
Un servicio es una clase anotada con @Service que encapsula la lógica de negocio. Procesa datos, aplica reglas de negocio y prepara información para los controladores.
1.1 Responsabilidades principales
| Responsabilidad | Descripción | Ejemplo |
|---|---|---|
| Lógica de negocio | Reglas específicas del dominio | Calcular descuentos |
| Transformación | Convierte DTOs ↔ Entidades | Mapeo de datos |
| Validación | Validaciones de negocio | Verificar stock |
| Transacciones | Consistencia de datos | Operaciones atómicas |
1.2 Ventajas
- Separación de responsabilidades: Controladores manejan HTTP, servicios la lógica
- Reutilización: Un servicio puede usarse en múltiples controladores
- Testabilidad: Fácil de probar con mocks
- Mantenibilidad: Cambios centralizados
2. Estructura básica
package com.ejemplo.demo.service;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
@Service // Marca la clase como servicio
@Transactional // Manejo automático de transacciones
public class ProductoService {
@Autowired // Inyección de dependencias
private ProductoRepository productoRepository;
// Métodos con lógica de negocio
public ProductoDTO crearProducto(CrearProductoDTO dto) {
// Lógica aquí
}
}
2.1 Anotaciones importantes
@Service
- Marca la clase como servicio
- Spring la detecta automáticamente
- Especialización de
@Component
@Transactional
- Manejo automático de transacciones
- Rollback automático en caso de error
- Se puede usar a nivel de clase o método
3. Implementación práctica
3.1 Entidades y DTOs
// Entidad
@Entity
@Table(name = "productos")
@NoArgsConstructor
@AllArgsConstructor
public class Producto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nombre;
private String descripcion;
@Column(nullable = false)
private BigDecimal precio;
@Column(nullable = false)
private Integer stock;
@Column(nullable = false)
private Boolean activo = true;
// getters y setters...
}
// DTO para transferencia
@Data
public class ProductoDTO {
private Long id;
private String nombre;
private String descripcion;
private BigDecimal precio;
private Integer stock;
private Boolean activo;
}
// DTO para creación
@Data
public class CrearProductoDTO {
@NotBlank(message = "El nombre es obligatorio")
private String nombre;
private String descripcion;
@NotNull(message = "El precio es obligatorio")
@DecimalMin(value = "0.01", message = "El precio debe ser mayor a 0")
private BigDecimal precio;
@NotNull(message = "El stock es obligatorio")
@Min(value = 0, message = "El stock no puede ser negativo")
private Integer stock;
}
3.2 Repositorio
@Repository
public interface ProductoRepository extends JpaRepository<Producto, Long> {
List<Producto> findByActivo(Boolean activo);
List<Producto> findByNombreContainingIgnoreCase(String nombre);
}
3.3 Servicio básico
@Service
@Transactional
public class ProductoService {
@Autowired
private ProductoRepository productoRepository;
// Crear producto
public ProductoDTO crearProducto(CrearProductoDTO dto) {
// 1. Validar datos
if (dto.getNombre() == null || dto.getNombre().trim().isEmpty()) {
throw new IllegalArgumentException("El nombre es obligatorio");
}
// 2. Convertir DTO a entidad
Producto producto = new Producto();
producto.setNombre(dto.getNombre());
producto.setDescripcion(dto.getDescripcion());
producto.setPrecio(dto.getPrecio());
producto.setStock(dto.getStock());
producto.setActivo(true);
// 3. Guardar
Producto guardado = productoRepository.save(producto);
// 4. Convertir a DTO y retornar
return convertirADTO(guardado);
}
// Obtener por ID
@Transactional(readOnly = true)
public ProductoDTO obtenerPorId(Long id) {
Producto producto = productoRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Producto no encontrado"));
return convertirADTO(producto);
}
// Listar productos activos
@Transactional(readOnly = true)
public List<ProductoDTO> obtenerActivos() {
List<Producto> productos = productoRepository.findByActivo(true);
return productos.stream()
.map(this::convertirADTO)
.collect(Collectors.toList());
}
// Buscar por nombre
@Transactional(readOnly = true)
public List<ProductoDTO> buscarPorNombre(String nombre) {
List<Producto> productos = productoRepository.findByNombreContainingIgnoreCase(nombre);
return productos.stream()
.map(this::convertirADTO)
.collect(Collectors.toList());
}
// Método auxiliar para convertir entidad a DTO
private ProductoDTO convertirADTO(Producto producto) {
ProductoDTO dto = new ProductoDTO();
dto.setId(producto.getId());
dto.setNombre(producto.getNombre());
dto.setDescripcion(producto.getDescripcion());
dto.setPrecio(producto.getPrecio());
dto.setStock(producto.getStock());
dto.setActivo(producto.getActivo());
return dto;
}
}
```
4. Ejercicios prácticos
Ejercicio 1: Crear un servicio básico
Crea un servicio UsuarioService con métodos para:
- Crear usuario
- Buscar por email
- Listar usuarios activos
Ejercicio 2: Agregar validaciones
Añade validaciones al ProductoService para:
- Verificar que el precio sea positivo
- Validar que el stock no sea negativo
- Comprobar que el nombre no esté vacío
Ejercicio 3: Método de negocio
Implementa un método calcularDescuento() que:
- Aplique 10% de descuento si el stock > 100
- Aplique 5% de descuento si el stock > 50
- Sin descuento si el stock ≤ 50
5. Puntos clave
Recuerda
- Los servicios contienen la lógica de negocio
- Usa
@Servicepara marcar la clase @Transactionalmaneja las transacciones automáticamente- Convierte siempre entre DTOs y entidades
- Valida los datos antes de procesarlos